Prometheus: Remove grafana-prometheus package#122953 (#123035)

* delete grafana-prometheus package

* delete grafana-prometheus package related configurations
This commit is contained in:
ismail simsek
2026-04-20 12:31:39 +02:00
committed by GitHub
parent f42e9e4169
commit f8176bfc9d
217 changed files with 20 additions and 47432 deletions

3
.github/CODEOWNERS vendored
View File

@@ -642,9 +642,6 @@ i18next.config.ts @grafana/grafana-frontend-platform
# @grafana/plugin-configs
/packages/grafana-plugin-configs/ @grafana/grafana-frontend-platform
# @grafana/prometheus
/packages/grafana-prometheus/ @grafana/data-sources-plugins
# @grafana/runtime
/packages/grafana-runtime/CHANGELOG.md @grafana/grafana-frontend-platform
/packages/grafana-runtime/LICENSE_APACHE2 @grafana/grafana-frontend-platform

View File

@@ -13,4 +13,4 @@ jobs:
crowdin_project_id: 5
pr_labels: 'area/frontend, area/internationalization, no-changelog, no-backport'
github_board_id: 78 # Frontend Platform project
en_paths: public/locales/en-US/grafana.json, public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json, public/app/plugins/datasource/mssql/locales/en-US/mssql.json, packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json, packages/grafana-sql/src/locales/en-US/grafana-sql.json
en_paths: public/locales/en-US/grafana.json, public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json, public/app/plugins/datasource/mssql/locales/en-US/mssql.json, packages/grafana-sql/src/locales/en-US/grafana-sql.json

View File

@@ -8,7 +8,6 @@ on:
- 'public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json'
- 'public/app/plugins/datasource/mssql/locales/en-US/mssql.json'
- 'packages/grafana-sql/src/locales/en-US/grafana-sql.json'
- 'packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json'
branches:
- main

View File

@@ -27,10 +27,4 @@ files: [
"type": "i18next_json",
"dest": "packages/grafana-sql/en-US/%original_file_name%"
},
{
"source": "packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json",
"translation": "packages/grafana-prometheus/src/locales/%locale%/%original_file_name%",
"type": "i18next_json",
"dest": "packages/grafana-prometheus/en-US/%original_file_name%"
},
]

View File

@@ -342,89 +342,6 @@
"count": 1
}
},
"packages/grafana-prometheus/src/components/PromExploreExtraField.tsx": {
"@grafana/no-gf-form": {
"count": 4
}
},
"packages/grafana-prometheus/src/components/PromQueryField.tsx": {
"@grafana/no-gf-form": {
"count": 2
}
},
"packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.ts": {
"@grafana/no-direct-local-storage-access": {
"count": 6
}
},
"packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx": {
"@grafana/no-gf-form": {
"count": 5
}
},
"packages/grafana-prometheus/src/configuration/ExemplarSetting.tsx": {
"@grafana/no-gf-form": {
"count": 1
}
},
"packages/grafana-prometheus/src/datasource.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
},
"@typescript-eslint/no-explicit-any": {
"count": 3
}
},
"packages/grafana-prometheus/src/language_provider.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 8
}
},
"packages/grafana-prometheus/src/language_provider.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
}
},
"packages/grafana-prometheus/src/language_utils.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"packages/grafana-prometheus/src/querybuilder/components/LabelFilterItem.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 4
}
},
"packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"packages/grafana-prometheus/src/querybuilder/shared/OperationParamEditorRegistry.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 3
}
},
"packages/grafana-prometheus/src/querybuilder/shared/types.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"packages/grafana-prometheus/src/resource_clients.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 8
}
},
"packages/grafana-prometheus/src/types.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 3
}
},
"packages/grafana-runtime/src/analytics/types.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -3956,4 +3873,4 @@
"count": 1
}
}
}
}

View File

@@ -385,7 +385,6 @@ module.exports = [
'packages/grafana-ui/**/*.{ts,tsx,js,jsx}',
'packages/grafana-data/**/*.{ts,tsx,js,jsx}',
'packages/grafana-sql/**/*.{ts,tsx,js,jsx}',
'packages/grafana-prometheus/**/*.{ts,tsx,js,jsx}',
...pluginsToTranslate.map((plugin) => `${plugin}/**/*.{ts,tsx,js,jsx}`),
],
ignores: [

View File

@@ -1,3 +0,0 @@
# (2024-02-16)
First public release. This release provides Prometheus exports in Grafana. Please be aware this is in the alpha state and there is likely to be breaking changes.

View File

@@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View File

@@ -1,13 +0,0 @@
# Grafana Prometheus Library
> **@grafana/prometheus is currently in ALPHA**.
@grafana/prometheus is a collection of components used to build a Prometheus data source plugin in [Grafana](https://github.com/grafana/grafana).
See [package source](https://github.com/grafana/grafana/tree/main/packages/grafana-prometheus) for more details.
## Installation
`yarn add @grafana/prometheus`
`npm install @grafana/prometheus`

View File

@@ -1,13 +0,0 @@
import { defineConfig } from 'i18next-cli';
export default defineConfig({
locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages
extract: {
input: ['src/**/*.{tsx,ts}'],
output: 'src/locales/{{language}}/{{namespace}}.json',
defaultNS: 'grafana-prometheus',
functions: ['t', '*.t'],
transComponents: ['Trans'],
warnOnConflicts: 'error',
},
});

View File

@@ -1,5 +0,0 @@
const sharedConfig = require('../../jest.config.js');
module.exports = {
...sharedConfig,
rootDir: '../../',
};

View File

@@ -1,118 +0,0 @@
{
"author": "Grafana Labs",
"license": "AGPL-3.0-only",
"name": "@grafana/prometheus",
"private": true,
"version": "13.1.0-pre",
"description": "Grafana Prometheus Library",
"keywords": [
"typescript",
"prometheus",
"grafana"
],
"sideEffects": false,
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-prometheus"
},
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"@grafana-app/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
}
},
"files": [
"./dist",
"./README.md",
"./CHANGELOG.md",
"./LICENSE_AGPL"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
"bundle": "rollup -c rollup.config.ts --configPlugin esbuild",
"clean": "rimraf ./dist ./compiled ./package.tgz",
"i18n-extract": "i18next-cli extract --sync-primary",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json"
},
"dependencies": {
"@emotion/css": "11.13.5",
"@floating-ui/react": "0.27.19",
"@grafana/assistant": "0.1.24",
"@grafana/data": "13.1.0-pre",
"@grafana/e2e-selectors": "13.1.0-pre",
"@grafana/i18n": "13.1.0-pre",
"@grafana/plugin-ui": "^0.13.1",
"@grafana/runtime": "13.1.0-pre",
"@grafana/schema": "13.1.0-pre",
"@grafana/ui": "13.1.0-pre",
"@hello-pangea/dnd": "18.0.1",
"@leeoniya/ufuzzy": "1.0.19",
"@lezer/common": "1.5.2",
"@lezer/highlight": "1.2.3",
"@lezer/lr": "1.4.8",
"@prometheus-io/lezer-promql": "0.307.3",
"@types/debounce-promise": "3.1.9",
"@types/lodash": "4.17.20",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react-highlight-words": "0.20.0",
"@types/semver": "7.7.1",
"@types/uuid": "11.0.0",
"debounce-promise": "3.1.2",
"lodash": "^4.17.23",
"moment": "2.30.1",
"moment-timezone": "0.5.47",
"monaco-editor": "0.34.1",
"monaco-promql": "1.8.0",
"pluralize": "8.0.0",
"prismjs": "1.30.0",
"react-highlight-words": "0.21.0",
"react-use": "17.6.0",
"react-window": "1.8.11",
"rxjs": "7.8.2",
"semver": "7.7.4",
"uuid": "13.0.0"
},
"devDependencies": {
"@rollup/plugin-dynamic-import-vars": "2.1.5",
"@rollup/plugin-image": "3.0.3",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "16.0.1",
"@testing-library/jest-dom": "6.6.4",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/jest": "29.5.14",
"@types/node": "24.10.1",
"@types/pluralize": "^0.0.33",
"@types/prismjs": "1.26.5",
"esbuild": "0.25.8",
"i18next-cli": "^1.48.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-select-event": "5.5.1",
"rimraf": "6.0.1",
"rollup": "^4.60.1",
"rollup-plugin-esbuild": "6.2.1",
"rollup-plugin-node-externals": "^8.0.0",
"testing-library-selector": "0.3.1",
"typescript": "6.0.2"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}

View File

@@ -1,9 +0,0 @@
{
"name": "@grafana/prometheus",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"tags": ["scope:package", "type:ui"],
"targets": {
"build": {}
}
}

View File

@@ -1,18 +0,0 @@
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';
import image from '@rollup/plugin-image';
import json from '@rollup/plugin-json';
import { createRequire } from 'node:module';
import { cjsOutput, entryPoint, esmOutput, plugins } from '../rollup.config.parts';
const rq = createRequire(import.meta.url);
const pkg = rq('./package.json');
export default [
{
input: entryPoint,
plugins: [...plugins, image(), json(), dynamicImportVars()],
output: [cjsOutput(pkg, 'grafana-prometheus'), esmOutput(pkg, 'grafana-prometheus')],
treeshake: false,
},
];

View File

@@ -1,144 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/add_label_to_query.test.ts
import { addLabelToQuery } from './add_label_to_query';
describe('addLabelToQuery()', () => {
it('should add label to simple query', () => {
expect(() => {
addLabelToQuery('foo', '', '');
}).toThrow();
expect(addLabelToQuery('foo', 'bar', 'baz')).toBe('foo{bar="baz"}');
expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}');
expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{x="yy", bar="baz"}');
expect(addLabelToQuery('metric > 0.001', 'foo', 'bar')).toBe('metric{foo="bar"} > 0.001');
});
it('should add custom operator', () => {
expect(addLabelToQuery('foo{}', 'bar', 'baz', '!=')).toBe('foo{bar!="baz"}');
expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '!=')).toBe('foo{x="yy", bar!="baz"}');
});
it('should not modify ranges', () => {
expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])');
});
it('should detect in-order function use', () => {
expect(addLabelToQuery('sum by (xx) (foo)', 'bar', 'baz')).toBe('sum by (xx) (foo{bar="baz"})');
});
it('should convert number Infinity to +Inf', () => {
expect(
addLabelToQuery('sum(rate(prometheus_tsdb_compaction_chunk_size_bytes_bucket[5m])) by (le)', 'le', Infinity)
).toBe('sum(rate(prometheus_tsdb_compaction_chunk_size_bytes_bucket{le="+Inf"}[5m])) by (le)');
});
it('should handle selectors with punctuation', () => {
expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe(
'foo{instance="my-host.com:9100", bar="baz"}'
);
expect(addLabelToQuery('foo:metric:rate1m', 'bar', 'baz')).toBe('foo:metric:rate1m{bar="baz"}');
expect(addLabelToQuery('avg(foo:metric:rate1m{a="b"})', 'bar', 'baz')).toBe(
'avg(foo:metric:rate1m{a="b", bar="baz"})'
);
expect(addLabelToQuery('foo{list="a,b,c"}', 'bar', 'baz')).toBe('foo{list="a,b,c", bar="baz"}');
});
it('should work on arithmetical expressions', () => {
expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}');
expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{x="yy", bar="baz"} + metric{bar="baz"}');
expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})');
expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe(
'foo{x="yy", bar="baz"} * metric{y="zz", a="bb", bar="baz"} * metric2{bar="baz"}'
);
});
it('should not add duplicate labels to a query', () => {
expect(addLabelToQuery(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '!='), 'bar', 'baz', '!=')).toBe(
'foo{x="yy", bar!="baz"}'
);
expect(addLabelToQuery(addLabelToQuery('rate(metric[1m])', 'foo', 'bar'), 'foo', 'bar')).toBe(
'rate(metric{foo="bar"}[1m])'
);
expect(addLabelToQuery(addLabelToQuery('foo{list="a,b,c"}', 'bar', 'baz'), 'bar', 'baz')).toBe(
'foo{list="a,b,c", bar="baz"}'
);
expect(addLabelToQuery(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz'), 'bar', 'baz')).toBe(
'avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})'
);
});
it('should modify existing labels if the operator is different', () => {
expect(addLabelToQuery(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '!='), 'bar', 'baz', '=')).toBe(
'foo{x="yy", bar="baz"}'
);
expect(addLabelToQuery(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '='), 'bar', 'baz', '!=')).toBe(
'foo{x="yy", bar!="baz"}'
);
});
it('should not remove filters', () => {
expect(addLabelToQuery('{x="y"} |="yy"', 'bar', 'baz')).toBe('{x="y", bar="baz"} |="yy"');
expect(addLabelToQuery('{x="y"} |="yy" !~"xx"', 'bar', 'baz')).toBe('{x="y", bar="baz"} |="yy" !~"xx"');
});
it('should add labels to metrics with logical operators', () => {
expect(addLabelToQuery('foo_info or bar_info', 'bar', 'baz')).toBe('foo_info{bar="baz"} or bar_info{bar="baz"}');
expect(addLabelToQuery('foo_info and bar_info', 'bar', 'baz')).toBe('foo_info{bar="baz"} and bar_info{bar="baz"}');
});
it('should not add ad-hoc filter to template variables', () => {
expect(addLabelToQuery('sum(rate({job="foo"}[2m])) by (value $variable)', 'bar', 'baz')).toBe(
'sum(rate({job="foo", bar="baz"}[2m])) by (value $variable)'
);
});
it('should not add ad-hoc filter to range', () => {
expect(addLabelToQuery('avg(rate((my_metric{job="foo"} > 0)[3h:])) by (label)', 'bar', 'baz')).toBe(
'avg(rate((my_metric{job="foo", bar="baz"} > 0)[3h:])) by (label)'
);
});
it('should not add ad-hoc filter to labels in label list provided with the group modifier', () => {
expect(
addLabelToQuery(
'max by (id, name, type) (my_metric{type=~"foo|bar|baz-test"}) * on(id) group_right(id, type, name) sum by (id) (my_metric) * 1000',
'bar',
'baz'
)
).toBe(
'max by (id, name, type) (my_metric{type=~"foo|bar|baz-test", bar="baz"}) * on(id) group_right(id, type, name) sum by (id) (my_metric{bar="baz"}) * 1000'
);
});
it('should not add ad-hoc filter to labels in label list provided with the group modifier', () => {
expect(addLabelToQuery('rate(my_metric[${__range_s}s])', 'bar', 'baz')).toBe(
'rate(my_metric{bar="baz"}[${__range_s}s])'
);
});
it('should not add ad-hoc filter to labels to math operations', () => {
expect(addLabelToQuery('count(my_metric{job!="foo"} < (5*1024*1024*1024) or vector(0)) - 1', 'bar', 'baz')).toBe(
'count(my_metric{job!="foo", bar="baz"} < (5*1024*1024*1024) or vector(0)) - 1'
);
});
it('should not add ad-hoc filter bool operator', () => {
expect(addLabelToQuery('ALERTS < bool 1', 'bar', 'baz')).toBe('ALERTS{bar="baz"} < bool 1');
});
it('should add a utf8 label', () => {
expect(addLabelToQuery('{"metric.name"}', 'cenk.erdem', 'muhabbet')).toBe(
'{"metric.name", "cenk.erdem"="muhabbet"}'
);
expect(addLabelToQuery('metric{label="val"}', 'cenk.erdem', 'muhabbet')).toBe(
'metric{label="val", "cenk.erdem"="muhabbet"}'
);
});
it('should not add a utf8 label when it is already applied', () => {
expect(addLabelToQuery('{"metric.name", "cenk.erdem"="muhabbet"}', 'cenk.erdem', 'muhabbet')).toBe(
'{"metric.name", "cenk.erdem"="muhabbet"}'
);
expect(addLabelToQuery('metric{label="val", "cenk.erdem"="muhabbet"}', 'cenk.erdem', 'muhabbet')).toBe(
'metric{label="val", "cenk.erdem"="muhabbet"}'
);
});
});

View File

@@ -1,109 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/add_label_to_query.ts
import { parser, VectorSelector } from '@prometheus-io/lezer-promql';
import { buildVisualQueryFromString } from './querybuilder/parsing';
import { renderQuery } from './querybuilder/shared/rendering/query';
import { type QueryBuilderLabelFilter } from './querybuilder/shared/types';
import { type PromVisualQuery } from './querybuilder/types';
/**
* Adds label filter to existing query. Useful for query modification for example for ad hoc filters.
*
* It uses PromQL parser to find instances of metric and labels, alters them and then splices them back into the query.
* Ideally we could use the parse -> change -> render is a simple 3 steps but right now building the visual query
* object does not support all possible queries.
*
* So instead this just operates on substrings of the query with labels and operates just on those. This makes this
* more robust and can alter even invalid queries, and preserves in general the query structure and whitespace.
* @param query
* @param key
* @param value
* @param operator
*/
export function addLabelToQuery(query: string, key: string, value: string | number, operator = '='): string {
if (!key) {
throw new Error('Need label to add to query.');
}
const vectorSelectorPositions = getVectorSelectorPositions(query);
if (!vectorSelectorPositions.length) {
return query;
}
const filter = toLabelFilter(key, value, operator);
return addFilter(query, vectorSelectorPositions, filter);
}
type VectorSelectorPosition = { from: number; to: number; query: PromVisualQuery };
/**
* Parse the string and get all VectorSelector positions in the query together with parsed representation of the vector
* selector.
* @param query
*/
function getVectorSelectorPositions(query: string): VectorSelectorPosition[] {
const tree = parser.parse(query);
const positions: VectorSelectorPosition[] = [];
tree.iterate({
enter: ({ to, from, type }): false | void => {
if (type.id === VectorSelector) {
const visQuery = buildVisualQueryFromString(query.substring(from, to));
positions.push({ query: visQuery.query, from, to });
return false;
}
},
});
return positions;
}
function toLabelFilter(key: string, value: string | number, operator: string): QueryBuilderLabelFilter {
// We need to make sure that we convert the value back to string because it may be a number
const transformedValue = value === Infinity ? '+Inf' : value.toString();
return { label: key, op: operator, value: transformedValue };
}
function addFilter(
query: string,
vectorSelectorPositions: VectorSelectorPosition[],
filter: QueryBuilderLabelFilter
): string {
let newQuery = '';
let prev = 0;
for (let i = 0; i < vectorSelectorPositions.length; i++) {
// This is basically just doing splice on a string for each matched vector selector.
const match = vectorSelectorPositions[i];
const isLast = i === vectorSelectorPositions.length - 1;
const start = query.substring(prev, match.from);
const end = isLast ? query.substring(match.to) : '';
const labelToMatch = labelExists(match.query.labels, filter);
if (labelToMatch) {
// if label exists, check the operator, if it is different, update it.
// We don't want to add duplicate labels.
if (labelToMatch.op !== filter.op) {
match.query.labels = match.query.labels.map((label) =>
label.label === filter.label && label.value === filter.value ? filter : label
);
}
} else {
// label does not exist, add as is.
match.query.labels.push(filter);
}
const newLabels = renderQuery(match.query);
newQuery += start + newLabels + end;
prev = match.to;
}
return newQuery;
}
/**
* Check if label exists in the list of labels but ignore the operator.
* @param labels
* @param filter
*/
function labelExists(labels: QueryBuilderLabelFilter[], filter: QueryBuilderLabelFilter) {
return labels.find((label) => label.label === filter.label && label.value === filter.value);
}

View File

@@ -1,730 +0,0 @@
import { Observable, of } from 'rxjs';
import {
type AnnotationEvent,
type AnnotationQuery,
type DataFrame,
type Field,
FieldType,
renderLegendFormat,
} from '@grafana/data';
import { PrometheusAnnotationSupport } from './annotations';
import { type PrometheusDatasource } from './datasource';
import { type PromQuery } from './types';
// Mock dependencies
jest.mock('@grafana/data', () => {
const original = jest.requireActual('@grafana/data');
return {
...original,
rangeUtil: {
...original.rangeUtil,
intervalToSeconds: jest.fn().mockImplementation((interval: string) => {
if (interval === '60s') {
return 60;
}
if (interval === '30s') {
return 30;
}
if (interval === '2m0s') {
return 120;
}
return 60; // default
}),
},
renderLegendFormat: jest.fn().mockImplementation((format: string, labels: Record<string, string>) => {
if (!format) {
return '';
}
return format.replace(/\{\{(\w+)\}\}/g, (_: string, key: string) => labels[key] || '');
}),
};
});
describe('PrometheusAnnotationSupport', () => {
// Create mock datasource
const mockDatasource = {} as PrometheusDatasource;
const annotationSupport = PrometheusAnnotationSupport(mockDatasource);
// Mock the implementation to match our testing expectations
beforeEach(() => {
// Reset and setup mocks before each test
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('prepareAnnotation', () => {
it('should respect existing target values and not override them', () => {
const annotation: AnnotationQuery<PromQuery> & { expr?: string; step?: string } = {
expr: 'rate(prometheus_http_requests_total[5m])',
step: '10s',
refId: 'testRefId',
target: {
expr: 'original_expr',
refId: 'originalRefId',
legendFormat: 'test',
interval: 'original_interval',
},
datasource: { uid: 'prometheus' },
enable: true,
name: 'Prometheus Annotation',
iconColor: 'red',
};
const result = annotationSupport.prepareAnnotation!(annotation);
// Check target properties are preserved when already set
expect(result.target?.refId).toBe('originalRefId');
expect(result.target?.expr).toBe('original_expr');
expect(result.target?.interval).toBe('original_interval');
expect(result.target?.legendFormat).toBe('test');
// Check the original properties are removed
expect(result.expr).toBeUndefined();
expect(result.step).toBeUndefined();
});
it('should transfer properties from json to target when target values are not set', () => {
const annotation: AnnotationQuery<PromQuery> & { expr?: string; step?: string } = {
expr: 'rate(prometheus_http_requests_total[5m])',
step: '10s',
refId: 'testRefId',
target: {
expr: '', // Empty string - should be overridden
refId: '', // Empty string - should be overridden
legendFormat: 'test',
// interval not set
},
datasource: { uid: 'prometheus' },
enable: true,
name: 'Prometheus Annotation',
iconColor: 'red',
};
const result = annotationSupport.prepareAnnotation!(annotation);
// Check target properties are set from json when target values are empty
expect(result.target?.refId).toBe('testRefId');
expect(result.target?.expr).toBe('rate(prometheus_http_requests_total[5m])');
expect(result.target?.interval).toBe('10s');
expect(result.target?.legendFormat).toBe('test');
// Check the original properties are removed
expect(result.expr).toBeUndefined();
expect(result.step).toBeUndefined();
});
it('should use default refId if not provided in either target or json', () => {
const annotation: AnnotationQuery<PromQuery> & { expr?: string; step?: string } = {
expr: 'up',
step: '30s',
target: {
expr: '',
refId: '',
},
datasource: { uid: 'prometheus' },
enable: true,
name: 'Prometheus Annotation',
iconColor: 'red',
};
const result = annotationSupport.prepareAnnotation!(annotation);
expect(result.target?.refId).toBe('Anno');
expect(result.target?.expr).toBe('up');
expect(result.target?.interval).toBe('30s');
});
it('should handle undefined target', () => {
const annotation: AnnotationQuery<PromQuery> & { expr?: string; step?: string } = {
expr: 'up',
step: '30s',
datasource: { uid: 'prometheus' },
enable: true,
name: 'Prometheus Annotation',
iconColor: 'red',
};
const result = annotationSupport.prepareAnnotation!(annotation);
expect(result.target?.refId).toBe('Anno');
expect(result.target?.expr).toBe('up');
expect(result.target?.interval).toBe('30s');
});
it('should handle undefined expr and step', () => {
const annotation: AnnotationQuery<PromQuery> = {
target: {
expr: '',
refId: '',
},
datasource: { uid: 'prometheus' },
enable: true,
name: 'Prometheus Annotation',
iconColor: 'red',
};
const result = annotationSupport.prepareAnnotation!(annotation);
expect(result.target?.refId).toBe('Anno');
expect(result.target?.expr).toBe('');
expect(result.target?.interval).toBe('');
});
it('should handle empty strings vs undefined values correctly', () => {
const annotation: AnnotationQuery<PromQuery> & { expr?: string; step?: string } = {
expr: 'test_expr',
step: '5s',
target: {
expr: '', // Empty string
refId: 'target_refId',
// interval not set at all
},
datasource: { uid: 'prometheus' },
enable: true,
name: 'Prometheus Annotation',
iconColor: 'red',
};
const result = annotationSupport.prepareAnnotation!(annotation);
// refId is set in target - should be preserved
expect(result.target?.refId).toBe('target_refId');
// expr is empty in target - should be replaced with json.expr
expect(result.target?.expr).toBe('test_expr');
// interval not set in target - should be set from json.step
expect(result.target?.interval).toBe('5s');
});
});
describe('processEvents', () => {
it('should return empty observable when no frames are provided', () => {
const annotation = {
target: {} as PromQuery,
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
// Mock the implementation to match the real one
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return new Observable<undefined>(); // This is what the implementation does - creates an Observable that never emits
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, []);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, []);
});
it('should process single frame into annotation events', () => {
const annotation = {
target: {} as PromQuery,
tagKeys: 'instance',
titleFormat: '{{instance}}',
textFormat: 'value: {{value}}',
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
const timeValues = [1000, 2000];
const valueValues = [1, 1];
const mockLabels = { instance: 'server1', value: '100' };
const frame: DataFrame = {
name: 'test',
length: timeValues.length,
fields: [
createField('Time', FieldType.time, timeValues),
createField('Value', FieldType.number, valueValues, mockLabels),
],
meta: {
executedQueryString: 'Step: 60s',
},
};
// Create expected result
const expectedEvent: AnnotationEvent = {
time: 1000,
timeEnd: 2000,
annotation: annotation,
title: 'server1',
tags: ['server1'],
text: 'value: 100',
};
// Manually call renderLegendFormat with the expected arguments
// This simulates what happens inside the real implementation
renderLegendFormat('{{instance}}', mockLabels);
renderLegendFormat('value: {{value}}', mockLabels);
// Mock the implementation to return our expected output
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of([expectedEvent]);
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [frame]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [frame]);
// Verify renderLegendFormat was called correctly
expect(renderLegendFormat).toHaveBeenCalledWith('{{instance}}', mockLabels);
expect(renderLegendFormat).toHaveBeenCalledWith('value: {{value}}', mockLabels);
});
it('should handle multiple frames', () => {
const annotation = {
target: {} as PromQuery,
tagKeys: 'app',
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
const frame1: DataFrame = {
name: 'test1',
length: 2,
fields: [
createField('Time', FieldType.time, [1000, 2000]),
createField('Value', FieldType.number, [1, 1], { app: 'app1' }),
],
meta: {
executedQueryString: 'Step: 60s',
},
};
const frame2: DataFrame = {
name: 'test2',
length: 2,
fields: [
createField('Time', FieldType.time, [3000, 4000]),
createField('Value', FieldType.number, [1, 1], { app: 'app2' }),
],
meta: {
executedQueryString: 'Step: 60s',
},
};
// Create expected events
const expectedEvents = [
{
time: 1000,
timeEnd: 2000,
annotation: annotation,
title: '',
tags: ['app1'],
text: '',
},
{
time: 3000,
timeEnd: 4000,
annotation: annotation,
title: '',
tags: ['app2'],
text: '',
},
];
// Mock the implementation
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of(expectedEvents);
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [frame1, frame2]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [frame1, frame2]);
});
it('should group events within step intervals', () => {
const annotation = {
target: {} as PromQuery,
tagKeys: '',
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
// Create timestamps where some should be grouped and some not
// With 60s step (60000ms), events within that range will be grouped
const timeValues = [1000, 2000, 60000, 120000];
const valueValues = [1, 1, 1, 1];
const frame: DataFrame = {
name: 'test',
length: timeValues.length,
fields: [createField('Time', FieldType.time, timeValues), createField('Value', FieldType.number, valueValues)],
meta: {
executedQueryString: 'Step: 60s',
},
};
// Create expected events - grouped as per the implementation logic
const expectedEvents = [
{
time: 1000,
timeEnd: 2000,
annotation: annotation,
title: '',
tags: [],
text: '',
},
{
time: 60000,
timeEnd: 120000,
annotation: annotation,
title: '',
tags: [],
text: '',
},
];
// Mock the implementation
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of(expectedEvents);
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [frame]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [frame]);
});
it('should handle useValueForTime option', () => {
const annotation = {
target: {} as PromQuery,
useValueForTime: true,
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
const frame: DataFrame = {
name: 'test',
length: 2,
fields: [
createField('Time', FieldType.time, [1000, 2000]),
createField('Value', FieldType.number, ['3000', '4000']), // Values as strings for parseFloat
],
meta: {
executedQueryString: 'Step: 60s',
},
};
// Create expected events - time from value field
const expectedEvents = [
{
time: 3000,
timeEnd: 4000,
annotation: annotation,
title: '',
tags: [],
text: '',
},
];
// Mock the implementation
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of(expectedEvents);
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [frame]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [frame]);
});
it('should filter by zero values', () => {
const annotation = {
target: {} as PromQuery,
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
const frame: DataFrame = {
name: 'test',
length: 4,
fields: [
createField('Time', FieldType.time, [1000, 2000, 3000, 4000]),
createField('Value', FieldType.number, [1, 0, 1, 0]), // Only non-zero values create events
],
meta: {
executedQueryString: 'Step: 60s',
},
};
// Create expected events - only for non-zero values
const expectedEvents = [
{
time: 1000,
timeEnd: 1000,
annotation: annotation,
title: '',
tags: [],
text: '',
},
{
time: 3000,
timeEnd: 3000,
annotation: annotation,
title: '',
tags: [],
text: '',
},
];
// Mock the implementation
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of(expectedEvents);
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [frame]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [frame]);
});
it('should handle empty frames with no fields', () => {
const annotation = {
target: {} as PromQuery,
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
const emptyFrame: DataFrame = {
name: 'test',
length: 0,
fields: [],
};
// Create expected events - empty array for empty frame
const expectedEvents: AnnotationEvent[] = [];
// Mock the implementation
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of(expectedEvents);
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [emptyFrame]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [emptyFrame]);
});
// Additional tests from the old implementation
it('should handle inactive regions with gaps', () => {
const annotation = {
target: {} as PromQuery,
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
// Recreate the test case from the old implementation
const timeValues = [2 * 60000, 3 * 60000, 5 * 60000, 6 * 60000, 7 * 60000, 8 * 60000, 9 * 60000];
const valueValues = [1, 1, 1, 1, 1, 0, 1];
const frame: DataFrame = {
name: 'test',
length: timeValues.length,
fields: [createField('Time', FieldType.time, timeValues), createField('Value', FieldType.number, valueValues)],
meta: {
executedQueryString: 'Step: 60s',
},
};
// Expected regions based on the old test
const expectedEvents = [
{
time: 120000,
timeEnd: 180000,
annotation: annotation,
title: '',
tags: [],
text: '',
},
{
time: 300000,
timeEnd: 420000,
annotation: annotation,
title: '',
tags: [],
text: '',
},
{
time: 540000,
timeEnd: 540000,
annotation: annotation,
title: '',
tags: [],
text: '',
},
];
// Mock the implementation
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of(expectedEvents);
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [frame]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [frame]);
});
it('should handle single region', () => {
const annotation = {
target: {} as PromQuery,
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
const timeValues = [2 * 60000, 3 * 60000];
const valueValues = [1, 1];
const frame: DataFrame = {
name: 'test',
length: timeValues.length,
fields: [createField('Time', FieldType.time, timeValues), createField('Value', FieldType.number, valueValues)],
meta: {
executedQueryString: 'Step: 60s',
},
};
const expectedEvents = [
{
time: 120000,
timeEnd: 180000,
annotation: annotation,
title: '',
tags: [],
text: '',
},
];
// Mock the implementation
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of(expectedEvents);
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [frame]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [frame]);
});
it('should handle larger step parameter for grouping', () => {
const annotation = {
target: {} as PromQuery,
enable: true,
name: 'test',
iconColor: 'red',
datasource: { uid: 'prometheus' },
} as AnnotationQuery<PromQuery>;
// Data from the original test
const timeValues = [1 * 120000, 2 * 120000, 3 * 120000, 4 * 120000, 5 * 120000, 6 * 120000];
const valueValues = [1, 1, 0, 0, 1, 1];
// First test with default 60s step
const frame1: DataFrame = {
name: 'test',
length: timeValues.length,
fields: [createField('Time', FieldType.time, timeValues), createField('Value', FieldType.number, valueValues)],
meta: {
executedQueryString: 'Step: 60s',
},
};
// Expected results with default step
const expectedEvents1 = [
{ time: 120000, timeEnd: 120000 },
{ time: 240000, timeEnd: 240000 },
{ time: 600000, timeEnd: 600000 },
{ time: 720000, timeEnd: 720000 },
];
// Mock the implementation for default step
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of(expectedEvents1.map((e) => ({ ...e, annotation, title: '', tags: [], text: '' })));
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [frame1]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [frame1]);
// Now test with larger 2m step
const frame2: DataFrame = {
name: 'test',
length: timeValues.length,
fields: [createField('Time', FieldType.time, timeValues), createField('Value', FieldType.number, valueValues)],
meta: {
executedQueryString: 'Step: 2m0s',
},
};
// Expected results with larger step
const expectedEvents2 = [
{ time: 120000, timeEnd: 240000 },
{ time: 600000, timeEnd: 720000 },
];
// Mock the implementation for larger step
jest.spyOn(annotationSupport, 'processEvents').mockImplementation(() => {
return of(expectedEvents2.map((e) => ({ ...e, annotation, title: '', tags: [], text: '' })));
});
// Call the function but don't store the unused result
annotationSupport.processEvents!(annotation, [frame2]);
// Verify the mock was called with the right arguments
expect(annotationSupport.processEvents).toHaveBeenCalledWith(annotation, [frame2]);
});
});
describe('QueryEditor', () => {
it('should have a QueryEditor component', () => {
expect(annotationSupport.QueryEditor).toBeDefined();
});
});
});
// Helper function to create fields for testing
function createField(name: string, type: FieldType, values: unknown[], labels = {}): Field {
return {
name,
type,
values,
config: {},
labels,
};
}

View File

@@ -1,133 +0,0 @@
import { Observable, of } from 'rxjs';
import {
type AnnotationEvent,
type AnnotationQuery,
type AnnotationSupport,
type DataFrame,
rangeUtil,
renderLegendFormat,
} from '@grafana/data';
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
import { type PrometheusDatasource } from './datasource';
import { type PromQuery } from './types';
const ANNOTATION_QUERY_STEP_DEFAULT = '60s';
export const PrometheusAnnotationSupport = (ds: PrometheusDatasource): AnnotationSupport<PromQuery> => {
return {
QueryEditor: AnnotationQueryEditor,
prepareAnnotation(json: AnnotationQuery<PromQuery>): AnnotationQuery<PromQuery> {
// Initialize target if it doesn't exist
if (!json.target) {
json.target = {
expr: '',
refId: 'Anno',
};
}
// Create a new target, preserving existing values when present
json.target = {
...json.target,
refId: json.target.refId || json.refId || 'Anno',
expr: json.target.expr || json.expr || '',
interval: json.target.interval || json.step || '',
};
// Remove properties that have been transferred to target
delete json.expr;
delete json.step;
return json;
},
processEvents(anno: AnnotationQuery<PromQuery>, frames: DataFrame[]): Observable<AnnotationEvent[] | undefined> {
if (!frames.length) {
return new Observable<undefined>();
}
const { tagKeys = '', titleFormat = '', textFormat = '' } = anno;
const input = frames[0].meta?.executedQueryString || '';
const regex = /Step:\s*([\d\w]+)/;
const match = input.match(regex);
const stepValue = match ? match[1] : null;
const step = rangeUtil.intervalToSeconds(stepValue || ANNOTATION_QUERY_STEP_DEFAULT) * 1000;
const tagKeysArray = tagKeys.split(',');
const eventList: AnnotationEvent[] = [];
for (const frame of frames) {
if (frame.fields.length === 0) {
continue;
}
const timeField = frame.fields[0];
const valueField = frame.fields[1];
const labels = valueField?.labels || {};
const tags = Object.keys(labels)
.filter((label) => tagKeysArray.includes(label))
.map((label) => labels[label]);
const timeValueTuple: Array<[number, number]> = [];
let idx = 0;
valueField.values.forEach((value: string) => {
let timeStampValue: number;
let valueValue: number;
const time = timeField.values[idx];
// If we want to use value as a time, we use value as timeStampValue and valueValue will be 1
if (anno.useValueForTime) {
timeStampValue = Math.floor(parseFloat(value));
valueValue = 1;
} else {
timeStampValue = Math.floor(parseFloat(time));
valueValue = parseFloat(value);
}
idx++;
timeValueTuple.push([timeStampValue, valueValue]);
});
const activeValues = timeValueTuple.filter((value) => value[1] > 0);
const activeValuesTimestamps = activeValues.map((value) => value[0]);
// Instead of creating singular annotation for each active event we group events into region if they are less
// or equal to `step` apart.
let latestEvent: AnnotationEvent | null = null;
for (const timestamp of activeValuesTimestamps) {
// We already have event `open` and we have new event that is inside the `step` so we just update the end.
if (latestEvent && (latestEvent.timeEnd ?? 0) + step >= timestamp) {
latestEvent.timeEnd = timestamp;
continue;
}
// Event exists but new one is outside of the `step` so we add it to eventList.
if (latestEvent) {
eventList.push(latestEvent);
}
// We start a new region.
latestEvent = {
time: timestamp,
timeEnd: timestamp,
annotation: anno,
title: renderLegendFormat(titleFormat, labels),
tags,
text: renderLegendFormat(textFormat, labels),
};
}
// Finish up last point if we have one
if (latestEvent) {
latestEvent.timeEnd = activeValuesTimestamps[activeValuesTimestamps.length - 1];
eventList.push(latestEvent);
}
}
return of(eventList);
},
};
};

View File

@@ -1,149 +0,0 @@
import {
getCacheDurationInMinutes,
getDaysToCacheMetadata,
getDebounceTimeInMilliseconds,
buildCacheHeaders,
getDefaultCacheHeaders,
} from './caching';
import { PrometheusCacheLevel } from './types';
describe('caching', () => {
describe('getDebounceTimeInMilliseconds', () => {
it('should return 600ms for Medium cache level', () => {
expect(getDebounceTimeInMilliseconds(PrometheusCacheLevel.Medium)).toBe(600);
});
it('should return 1200ms for High cache level', () => {
expect(getDebounceTimeInMilliseconds(PrometheusCacheLevel.High)).toBe(1200);
});
it('should return 350ms for Low cache level', () => {
expect(getDebounceTimeInMilliseconds(PrometheusCacheLevel.Low)).toBe(350);
});
it('should return 350ms for None cache level', () => {
expect(getDebounceTimeInMilliseconds(PrometheusCacheLevel.None)).toBe(350);
});
it('should return default value (350ms) for unknown cache level', () => {
expect(getDebounceTimeInMilliseconds('invalid' as PrometheusCacheLevel)).toBe(350);
});
});
describe('getDaysToCacheMetadata', () => {
it('should return 7 days for Medium cache level', () => {
expect(getDaysToCacheMetadata(PrometheusCacheLevel.Medium)).toBe(7);
});
it('should return 30 days for High cache level', () => {
expect(getDaysToCacheMetadata(PrometheusCacheLevel.High)).toBe(30);
});
it('should return 1 day for Low cache level', () => {
expect(getDaysToCacheMetadata(PrometheusCacheLevel.Low)).toBe(1);
});
it('should return 1 day for None cache level', () => {
expect(getDaysToCacheMetadata(PrometheusCacheLevel.None)).toBe(1);
});
it('should return default value (1 day) for unknown cache level', () => {
expect(getDaysToCacheMetadata('invalid' as PrometheusCacheLevel)).toBe(1);
});
});
describe('getCacheDurationInMinutes', () => {
it('should return 10 minutes for Medium cache level', () => {
expect(getCacheDurationInMinutes(PrometheusCacheLevel.Medium)).toBe(10);
});
it('should return 60 minutes for High cache level', () => {
expect(getCacheDurationInMinutes(PrometheusCacheLevel.High)).toBe(60);
});
it('should return 1 minute for Low cache level', () => {
expect(getCacheDurationInMinutes(PrometheusCacheLevel.Low)).toBe(1);
});
it('should return 1 minute for None cache level', () => {
expect(getCacheDurationInMinutes(PrometheusCacheLevel.None)).toBe(1);
});
it('should return default value (1 minute) for unknown cache level', () => {
expect(getCacheDurationInMinutes('invalid' as PrometheusCacheLevel)).toBe(1);
});
});
describe('buildCacheHeaders', () => {
it('should build cache headers with provided duration in seconds', () => {
const result = buildCacheHeaders(300);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=300',
},
});
});
it('should handle zero duration', () => {
const result = buildCacheHeaders(0);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=0',
},
});
});
it('should handle large duration values', () => {
const oneDayInSeconds = 86400;
const result = buildCacheHeaders(oneDayInSeconds);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=86400',
},
});
});
});
describe('getDefaultCacheHeaders', () => {
it('should return cache headers for Medium cache level', () => {
const result = getDefaultCacheHeaders(PrometheusCacheLevel.Medium);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=600', // 10 minutes in seconds
},
});
});
it('should return cache headers for High cache level', () => {
const result = getDefaultCacheHeaders(PrometheusCacheLevel.High);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=3600', // 60 minutes in seconds
},
});
});
it('should return cache headers for Low cache level', () => {
const result = getDefaultCacheHeaders(PrometheusCacheLevel.Low);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=60', // 1 minute in seconds
},
});
});
it('should return undefined for None cache level', () => {
const result = getDefaultCacheHeaders(PrometheusCacheLevel.None);
expect(result).toBeUndefined();
});
it('should handle unknown cache level as default (1 minute)', () => {
const result = getDefaultCacheHeaders('invalid' as PrometheusCacheLevel);
expect(result).toEqual({
headers: {
'X-Grafana-Cache': 'private, max-age=60', // 1 minute in seconds
},
});
});
});
});

View File

@@ -1,101 +0,0 @@
import { PrometheusCacheLevel } from './types';
/**
* Returns the debounce time in milliseconds based on the cache level.
* Used to control the frequency of API requests.
*
* @param {PrometheusCacheLevel} cacheLevel - The cache level (None, Low, Medium, High)
* @returns {number} Debounce time in milliseconds:
* - Medium: 600ms
* - High: 1200ms
* - Default (None/Low): 350ms
*/
export const getDebounceTimeInMilliseconds = (cacheLevel: PrometheusCacheLevel): number => {
switch (cacheLevel) {
case PrometheusCacheLevel.Medium:
return 600;
case PrometheusCacheLevel.High:
return 1200;
default:
return 350;
}
};
/**
* Returns the number of days to cache metadata based on the cache level.
* Used for caching Prometheus metric metadata.
*
* @param {PrometheusCacheLevel} cacheLevel - The cache level (None, Low, Medium, High)
* @returns {number} Number of days to cache:
* - Medium: 7 days
* - High: 30 days
* - Default (None/Low): 1 day
*/
export const getDaysToCacheMetadata = (cacheLevel: PrometheusCacheLevel): number => {
switch (cacheLevel) {
case PrometheusCacheLevel.Medium:
return 7;
case PrometheusCacheLevel.High:
return 30;
default:
return 1;
}
};
/**
* Returns the cache duration in minutes based on the cache level.
* Used for general API response caching.
*
* @param {PrometheusCacheLevel} cacheLevel - The cache level (None, Low, Medium, High)
* @returns Cache duration in minutes:
* - Medium: 10 minutes
* - High: 60 minutes
* - Default (None/Low): 1 minute
*/
export const getCacheDurationInMinutes = (cacheLevel: PrometheusCacheLevel) => {
switch (cacheLevel) {
case PrometheusCacheLevel.Medium:
return 10;
case PrometheusCacheLevel.High:
return 60;
default:
return 1;
}
};
/**
* Builds cache headers for Prometheus API requests.
* Creates a standard cache control header with private scope and max-age directive.
*
* @param {number} durationInSeconds - Cache duration in seconds
* @returns Object containing headers with cache control directives:
* - X-Grafana-Cache: private, max-age=<duration>
* @example
* // Returns { headers: { 'X-Grafana-Cache': 'private, max-age=300' } }
* buildCacheHeaders(300)
*/
export const buildCacheHeaders = (durationInSeconds: number) => {
return {
headers: {
'X-Grafana-Cache': `private, max-age=${durationInSeconds}`,
},
};
};
/**
* Gets appropriate cache headers based on the configured cache level.
* Converts cache duration from minutes to seconds and builds the headers.
* Returns undefined if caching is disabled (None level).
*
* @param {PrometheusCacheLevel} cacheLevel - Cache level (None, Low, Medium, High)
* @returns Cache headers object or undefined if caching is disabled
* @example
* // For Medium level, returns { headers: { 'X-Grafana-Cache': 'private, max-age=600' } }
* getDefaultCacheHeaders(PrometheusCacheLevel.Medium)
*/
export const getDefaultCacheHeaders = (cacheLevel: PrometheusCacheLevel) => {
if (cacheLevel !== PrometheusCacheLevel.None) {
return buildCacheHeaders(getCacheDurationInMinutes(cacheLevel) * 60);
}
return;
};

View File

@@ -1,165 +0,0 @@
// Core Grafana testing pattern
import { fireEvent, render, screen } from '@testing-library/react';
import { type AnnotationQuery } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { type PrometheusDatasource } from '../datasource';
import { type PrometheusLanguageProviderInterface } from '../language_provider';
import { EmptyLanguageProviderMock } from '../language_provider.mock';
import { type PromQuery } from '../types';
import { AnnotationQueryEditor } from './AnnotationQueryEditor';
// Mock the PromQueryCodeEditor to avoid errors related to PromQueryField rendering
jest.mock('../querybuilder/components/PromQueryCodeEditor', () => ({
PromQueryCodeEditor: () => <div data-testid="mock-prom-code-editor">Query Editor</div>,
}));
describe('AnnotationQueryEditor', () => {
const mockOnChange = jest.fn();
const mockOnAnnotationChange = jest.fn();
const mockOnRunQuery = jest.fn();
const mockQuery: PromQuery = {
refId: 'test',
expr: 'test_metric',
interval: '',
exemplar: true,
instant: false,
range: true,
};
const mockAnnotation: AnnotationQuery<PromQuery> = {
name: 'Test annotation',
enable: true,
iconColor: 'red',
datasource: {
type: 'prometheus',
uid: 'test',
},
target: mockQuery,
hide: false,
titleFormat: '{{alertname}}',
textFormat: '{{instance}}',
tagKeys: 'label1,label2',
useValueForTime: false,
};
function createMockDatasource() {
const languageProvider = new EmptyLanguageProviderMock() as unknown as PrometheusLanguageProviderInterface;
const mockDatasource = {
languageProvider,
lookupsDisabled: false,
modifyQuery: jest.fn().mockImplementation((query) => query),
getQueryHints: jest.fn().mockReturnValue([]),
} as unknown as PrometheusDatasource;
return mockDatasource;
}
const defaultProps = {
query: mockQuery,
onChange: mockOnChange,
onRunQuery: mockOnRunQuery,
annotation: mockAnnotation,
onAnnotationChange: mockOnAnnotationChange,
datasource: createMockDatasource(),
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders without error', () => {
render(<AnnotationQueryEditor {...defaultProps} />);
expect(screen.getByText('Min step')).toBeInTheDocument();
expect(screen.getByText('Title')).toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Text')).toBeInTheDocument();
expect(screen.getByText('Series value as timestamp')).toBeInTheDocument();
expect(screen.getByTestId('mock-prom-code-editor')).toBeInTheDocument();
});
it('displays an error message when annotation data is missing', () => {
render(<AnnotationQueryEditor {...defaultProps} annotation={undefined} />);
expect(screen.getByText('Annotation data load error!')).toBeInTheDocument();
});
it('displays an error message when onAnnotationChange is missing', () => {
render(<AnnotationQueryEditor {...defaultProps} onAnnotationChange={undefined} />);
expect(screen.getByText('Annotation data load error!')).toBeInTheDocument();
});
it('renders correctly with an empty annotation object', () => {
render(<AnnotationQueryEditor {...defaultProps} annotation={{} as AnnotationQuery<PromQuery>} />);
// Should render normally with empty values but not show an error
expect(screen.getByText('Min step')).toBeInTheDocument();
expect(screen.getByText('Title')).toBeInTheDocument();
expect(screen.queryByText('Annotation data load error!')).not.toBeInTheDocument();
});
it('calls onChange when min step is updated', () => {
render(<AnnotationQueryEditor {...defaultProps} />);
const minStepInput = screen.getByLabelText('Set lower limit for the step parameter');
// Instead of typing character by character, use a direct value change
fireEvent.change(minStepInput, { target: { value: '10s' } });
fireEvent.blur(minStepInput);
expect(mockOnChange).toHaveBeenCalledWith({
...mockQuery,
interval: '10s',
});
});
it('calls onAnnotationChange when title format is updated', () => {
render(<AnnotationQueryEditor {...defaultProps} />);
const titleInput = screen.getByTestId(selectors.components.DataSource.Prometheus.annotations.title);
fireEvent.change(titleInput, { target: { value: '{{job}}' } });
fireEvent.blur(titleInput);
expect(mockOnAnnotationChange).toHaveBeenCalledWith({
...mockAnnotation,
titleFormat: '{{job}}',
});
});
it('calls onAnnotationChange when tags are updated', () => {
render(<AnnotationQueryEditor {...defaultProps} />);
const tagsInput = screen.getByTestId(selectors.components.DataSource.Prometheus.annotations.tags);
fireEvent.change(tagsInput, { target: { value: 'job,instance' } });
fireEvent.blur(tagsInput);
expect(mockOnAnnotationChange).toHaveBeenCalledWith({
...mockAnnotation,
tagKeys: 'job,instance',
});
});
it('calls onAnnotationChange when text format is updated', () => {
render(<AnnotationQueryEditor {...defaultProps} />);
const textInput = screen.getByTestId(selectors.components.DataSource.Prometheus.annotations.text);
fireEvent.change(textInput, { target: { value: '{{metric}}' } });
fireEvent.blur(textInput);
expect(mockOnAnnotationChange).toHaveBeenCalledWith({
...mockAnnotation,
textFormat: '{{metric}}',
});
});
it('calls onAnnotationChange when series value as timestamp is toggled', () => {
render(<AnnotationQueryEditor {...defaultProps} />);
const toggle = screen.getByTestId(selectors.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp);
fireEvent.click(toggle);
expect(mockOnAnnotationChange).toHaveBeenCalledWith({
...mockAnnotation,
useValueForTime: true,
});
});
});

View File

@@ -1,170 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx
import { memo } from 'react';
import { type AnnotationQuery } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { EditorField, EditorRow, EditorRows, EditorSwitch } from '@grafana/plugin-ui';
import { AutoSizeInput, Input, Space } from '@grafana/ui';
import { PromQueryCodeEditor } from '../querybuilder/components/PromQueryCodeEditor';
import { type PromQuery } from '../types';
import { type PromQueryEditorProps } from './types';
type Props = PromQueryEditorProps & {
annotation?: AnnotationQuery<PromQuery>;
onAnnotationChange?: (annotation: AnnotationQuery<PromQuery>) => void;
};
const PLACEHOLDER_TITLE = '{{alertname}}';
const PLACEHOLDER_TEXT = '{{instance}}';
const PLACEHOLDER_TAGS = 'label1,label2';
/**
* AnnotationQueryEditor component for Prometheus datasource.
* Allows users to configure annotation queries with options for title, tags, text format,
* and timestamp settings.
*/
export const AnnotationQueryEditor = memo(function AnnotationQueryEditor(props: Props) {
const { annotation, onAnnotationChange, onChange, onRunQuery, query } = props;
if (!annotation || !onAnnotationChange) {
return (
<h3>
<Trans i18nKey="grafana-prometheus.components.annotation-query-editor.annotation-data-load-error">
Annotation data load error!
</Trans>
</h3>
);
}
const handleMinStepChange = (value: string) => {
onChange({ ...query, interval: value });
};
const handleTitleChange = (value: string) => {
onAnnotationChange({
...annotation,
titleFormat: value,
});
};
const handleTagsChange = (value: string) => {
onAnnotationChange({
...annotation,
tagKeys: value,
});
};
const handleTextChange = (value: string) => {
onAnnotationChange({
...annotation,
textFormat: value,
});
};
const handleUseValueForTimeChange = (checked: boolean) => {
onAnnotationChange({
...annotation,
useValueForTime: checked,
});
};
return (
<>
<EditorRows>
<PromQueryCodeEditor {...props} query={query} showExplain={false} onRunQuery={onRunQuery} onChange={onChange} />
<EditorRow>
<EditorField
label={t('grafana-prometheus.components.annotation-query-editor.label-min-step', 'Min step')}
tooltip={
<Trans
i18nKey="grafana-prometheus.components.annotation-query-editor.tooltip-min-step"
values={{ intervalVar: '$__interval', rateIntervalVar: '$__rate_interval' }}
>
An additional lower limit for the step parameter of the Prometheus query and for the{' '}
<code>{'{{intervalVar}}'}</code> and <code>{'{{rateIntervalVar}}'}</code> variables.
</Trans>
}
>
<AutoSizeInput
type="text"
aria-label={t(
'grafana-prometheus.components.annotation-query-editor.aria-label-lower-limit-parameter',
'Set lower limit for the step parameter'
)}
placeholder={t('grafana-prometheus.components.annotation-query-editor.placeholder-auto', 'auto')}
minWidth={10}
value={query.interval ?? ''}
onChange={(e) => handleMinStepChange(e.currentTarget.value)}
id={selectors.components.DataSource.Prometheus.annotations.minStep}
data-testid={selectors.components.DataSource.Prometheus.annotations.minStep}
/>
</EditorField>
</EditorRow>
</EditorRows>
<Space v={0.5} />
<EditorRow>
<EditorField
label={t('grafana-prometheus.components.annotation-query-editor.label-title', 'Title')}
tooltip={t(
'grafana-prometheus.components.annotation-query-editor.tooltip-either-pattern-example-instance-replaced-label',
'Use either the name or a pattern. For example, {{labelTemplate}} is replaced with label value for the label {{labelName}}.',
{ labelName: 'instance', labelTemplate: '{{instance}}' }
)}
>
<Input
type="text"
placeholder={PLACEHOLDER_TITLE}
value={annotation.titleFormat ?? ''}
onChange={(event) => handleTitleChange(event.currentTarget.value)}
data-testid={selectors.components.DataSource.Prometheus.annotations.title}
/>
</EditorField>
<EditorField label={t('grafana-prometheus.components.annotation-query-editor.label-tags', 'Tags')}>
<Input
type="text"
placeholder={PLACEHOLDER_TAGS}
value={annotation.tagKeys ?? ''}
onChange={(event) => handleTagsChange(event.currentTarget.value)}
data-testid={selectors.components.DataSource.Prometheus.annotations.tags}
/>
</EditorField>
<EditorField
label={t('grafana-prometheus.components.annotation-query-editor.label-text', 'Text')}
tooltip={t(
'grafana-prometheus.components.annotation-query-editor.tooltip-either-pattern-example-instance-replaced-label',
'Use either the name or a pattern. For example, {{labelTemplate}} is replaced with label value for the label {{labelName}}.',
{ labelName: 'instance', labelTemplate: '{{instance}}' }
)}
>
<Input
type="text"
placeholder={PLACEHOLDER_TEXT}
value={annotation.textFormat ?? ''}
onChange={(event) => handleTextChange(event.currentTarget.value)}
data-testid={selectors.components.DataSource.Prometheus.annotations.text}
/>
</EditorField>
<EditorField
label={t(
'grafana-prometheus.components.annotation-query-editor.label-series-value-as-timestamp',
'Series value as timestamp'
)}
tooltip={t(
'grafana-prometheus.components.annotation-query-editor.tooltip-timestamp-milliseconds-series-value-seconds-multiply',
'The unit of timestamp is milliseconds. If the unit of the series value is seconds, multiply its range vector by 1000.'
)}
>
<EditorSwitch
value={annotation.useValueForTime ?? false}
onChange={(event) => handleUseValueForTimeChange(event.currentTarget.checked)}
data-testid={selectors.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp}
/>
</EditorField>
</EditorRow>
</>
);
});

View File

@@ -1,76 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx
import { css } from '@emotion/css';
import { type GrafanaTheme2, type QueryEditorHelpProps } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';
import { type PromQuery } from '../types';
const CHEAT_SHEET_ITEMS = [
{
title: 'Request Rate',
expression: 'rate(http_request_total[5m])',
label:
'Given an HTTP request counter, this query calculates the per-second average request rate over the last 5 minutes.',
},
{
title: '95th Percentile of Request Latencies',
expression: 'histogram_quantile(0.95, sum(rate(prometheus_http_request_duration_seconds_bucket[5m])) by (le))',
label: 'Calculates the 95th percentile of HTTP request rate over 5 minute windows.',
},
{
title: 'Alerts Firing',
expression: 'sort_desc(sum(sum_over_time(ALERTS{alertstate="firing"}[24h])) by (alertname))',
label: 'Sums up the alerts that have been firing over the last 24 hours.',
},
{
title: 'Step',
label:
'Defines the graph resolution using a duration format (15s, 1m, 3h, ...). Small steps create high-resolution graphs but can be slow over larger time ranges. Using a longer step lowers the resolution and smooths the graph by producing fewer datapoints. If no step is given the resolution is calculated automatically.',
},
];
export const PromCheatSheet = (props: QueryEditorHelpProps<PromQuery>) => {
const styles = useStyles2(getStyles);
return (
<div>
<h2>
<Trans i18nKey="grafana-prometheus.components.prom-cheat-sheet.prom-ql-cheat-sheet">PromQL Cheat Sheet</Trans>
</h2>
{CHEAT_SHEET_ITEMS.map((item, index) => (
<div className={styles.cheatSheetItem} key={index}>
<div className={styles.cheatSheetItemTitle}>{item.title}</div>
{item.expression ? (
<button
type="button"
className={styles.cheatSheetExample}
onClick={(e) => props.onClickExample({ refId: 'A', expr: item.expression })}
>
<code>{item.expression}</code>
</button>
) : null}
{item.label}
</div>
))}
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
cheatSheetItem: css({
margin: theme.spacing(3, 0),
}),
cheatSheetItemTitle: css({
fontSize: theme.typography.h3.fontSize,
}),
cheatSheetExample: css({
margin: theme.spacing(0.5, 0),
// element is interactive, clear button styles
textAlign: 'left',
border: 'none',
background: 'transparent',
display: 'block',
}),
});

View File

@@ -1,91 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromExemplarField.tsx
import { css, cx } from '@emotion/css';
import { useEffect, useState } from 'react';
import { usePrevious } from 'react-use';
import { type GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { IconButton, InlineLabel, Tooltip, useStyles2 } from '@grafana/ui';
import { type PrometheusDatasource } from '../datasource';
import { type PromQuery } from '../types';
interface Props {
onChange: (exemplar: boolean) => void;
datasource: PrometheusDatasource;
query: PromQuery;
'data-testid'?: string;
}
export function PromExemplarField({ datasource, onChange, query, ...rest }: Props) {
const [error, setError] = useState<string | null>(null);
const styles = useStyles2(getStyles);
const prevError = usePrevious(error);
useEffect(() => {
if (!datasource.exemplarsAvailable) {
setError('Exemplars for this query are not available');
onChange(false);
} else if (query.instant && !query.range) {
setError('Exemplars are not available for instant queries');
onChange(false);
} else {
setError(null);
// If error is cleared, we want to change exemplar to true
if (prevError && !error) {
onChange(true);
}
}
}, [datasource.exemplarsAvailable, query.instant, query.range, onChange, prevError, error]);
const iconButtonStyles = cx(
{
[styles.activeIcon]: !!query.exemplar,
},
styles.eyeIcon
);
return (
<InlineLabel width="auto" data-testid={rest['data-testid']}>
<Tooltip content={error ?? ''}>
<div className={styles.iconWrapper}>
<Trans i18nKey="grafana-prometheus.components.prom-exemplar-field.exemplars">Exemplars</Trans>
<IconButton
name="eye"
tooltip={
!!query.exemplar
? t(
'grafana-prometheus.components.prom-exemplar-field.tooltip-disable-query',
'Disable query with exemplars'
)
: t(
'grafana-prometheus.components.prom-exemplar-field.tooltip-enable-query',
'Enable query with exemplars'
)
}
disabled={!!error}
className={iconButtonStyles}
onClick={() => {
onChange(!query.exemplar);
}}
/>
</div>
</Tooltip>
</InlineLabel>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
eyeIcon: css({
marginLeft: theme.spacing(2),
}),
activeIcon: css({
color: theme.colors.primary.main,
}),
iconWrapper: css({
display: 'flex',
alignItems: 'center',
}),
};
}

View File

@@ -1,41 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromExploreExtraField.test.tsx
import { render, screen } from '@testing-library/react';
import { type PrometheusDatasource } from '../datasource';
import { type PromQuery } from '../types';
import {
PromExploreExtraField,
type PromExploreExtraFieldProps,
promExploreExtraFieldTestIds,
} from './PromExploreExtraField';
const setup = (propOverrides?: PromExploreExtraFieldProps) => {
const query = { exemplar: false } as PromQuery;
const datasource = {} as PrometheusDatasource;
const onChange = jest.fn();
const onRunQuery = jest.fn();
const props: PromExploreExtraFieldProps = {
onChange,
onRunQuery,
query,
datasource,
};
Object.assign(props, propOverrides);
return render(<PromExploreExtraField {...props} />);
};
describe('PromExploreExtraField', () => {
it('should render step field', () => {
setup();
expect(screen.getByTestId(promExploreExtraFieldTestIds.stepField)).toBeInTheDocument();
});
it('should render query type field', () => {
setup();
expect(screen.getByTestId(promExploreExtraFieldTestIds.queryTypeField)).toBeInTheDocument();
});
});

View File

@@ -1,191 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromExploreExtraField.tsx
import { css, cx } from '@emotion/css';
import { isEqual } from 'lodash';
import { memo, useCallback } from 'react';
import * as React from 'react';
import { usePrevious } from 'react-use';
import { type GrafanaTheme2 } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { InlineFormLabel, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { type PrometheusDatasource } from '../datasource';
import { type PromQuery } from '../types';
import { PromExemplarField } from './PromExemplarField';
export interface PromExploreExtraFieldProps {
query: PromQuery;
onChange: (value: PromQuery) => void;
onRunQuery: () => void;
datasource: PrometheusDatasource;
}
export const PromExploreExtraField = memo(({ query, datasource, onChange, onRunQuery }: PromExploreExtraFieldProps) => {
const rangeOptions = getQueryTypeOptions(true);
const prevQuery = usePrevious(query);
const styles = useStyles2(getStyles);
const onExemplarChange = useCallback(
(exemplar: boolean) => {
if (!isEqual(query, prevQuery) || exemplar !== query.exemplar) {
onChange({ ...query, exemplar });
}
},
[prevQuery, query, onChange]
);
function onChangeQueryStep(interval: string) {
onChange({ ...query, interval });
}
function onStepChange(e: React.SyntheticEvent<HTMLInputElement>) {
if (e.currentTarget.value !== query.interval) {
onChangeQueryStep(e.currentTarget.value);
}
}
function onReturnKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter' && e.shiftKey) {
onRunQuery();
}
}
const onQueryTypeChange = getQueryTypeChangeHandler(query, onChange);
return (
<div
aria-label={t(
'grafana-prometheus.components.prom-explore-extra-field.aria-label-prometheus-extra-field',
'Prometheus extra field'
)}
className="gf-form-inline"
data-testid={promExploreExtraFieldTestIds.extraFieldEditor}
>
{/*Query type field*/}
<div
data-testid={promExploreExtraFieldTestIds.queryTypeField}
className={cx(
'gf-form',
styles.queryTypeField,
css({
flexWrap: 'nowrap',
})
)}
aria-label={t(
'grafana-prometheus.components.prom-explore-extra-field.aria-label-query-type-field',
'Query type field'
)}
>
<InlineFormLabel width="auto">
<Trans i18nKey="grafana-prometheus.components.prom-explore-extra-field.query-type">Query type</Trans>
</InlineFormLabel>
<RadioButtonGroup
options={rangeOptions}
value={query.range && query.instant ? 'both' : query.instant ? 'instant' : 'range'}
onChange={onQueryTypeChange}
/>
</div>
{/*Step field*/}
<div
data-testid={promExploreExtraFieldTestIds.stepField}
className={cx(
'gf-form',
css({
flexWrap: 'nowrap',
})
)}
aria-label={t('grafana-prometheus.components.prom-explore-extra-field.aria-label-step-field', 'Step field')}
>
<InlineFormLabel
width={6}
tooltip={t(
'grafana-prometheus.components.prom-explore-extra-field.tooltip-units-builtin-variables-example-interval-rateinterval',
'Time units and built-in variables can be used here, for example: {{example1}}, {{example2}}, {{example3}}, {{example4}}, {{example5}}, {{example6}}, {{example7}} (Default if no unit is specified: {{default}})',
{
example1: '$__interval',
example2: '$__rate_interval',
example3: '5s',
example4: '1m',
example5: '3h',
example6: '1d',
example7: '1y',
default: 's',
}
)}
>
<Trans i18nKey="grafana-prometheus.components.prom-explore-extra-field.min-step">Min step</Trans>
</InlineFormLabel>
<input
type={'text'}
className="gf-form-input width-4"
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="auto"
onChange={onStepChange}
onKeyDown={onReturnKeyDown}
value={query.interval ?? ''}
/>
</div>
<PromExemplarField onChange={onExemplarChange} datasource={datasource} query={query} />
</div>
);
});
PromExploreExtraField.displayName = 'PromExploreExtraField';
export function getQueryTypeOptions(includeBoth: boolean) {
const rangeOptions = [
{
value: 'range',
label: t('grafana-prometheus.components.get-query-type-options.range-options.label.range', 'Range'),
description: t(
'grafana-prometheus.components.get-query-type-options.range-options.description.query-range',
'Run query over a range of time'
),
},
{
value: 'instant',
label: t('grafana-prometheus.components.get-query-type-options.range-options.label.instant', 'Instant'),
description: 'Run query against a single point in time. For this query, the "To" time is used',
},
];
if (includeBoth) {
rangeOptions.push({
value: 'both',
label: t('grafana-prometheus.components.get-query-type-options.label.both', 'Both'),
description: t(
'grafana-prometheus.components.get-query-type-options.description.instant-query-range',
'Run an Instant query and a Range query'
),
});
}
return rangeOptions;
}
export function getQueryTypeChangeHandler(query: PromQuery, onChange: (update: PromQuery) => void) {
return (queryType: string) => {
if (queryType === 'instant') {
onChange({ ...query, instant: true, range: false, exemplar: false });
} else if (queryType === 'range') {
onChange({ ...query, instant: false, range: true });
} else {
onChange({ ...query, instant: true, range: true });
}
};
}
export const promExploreExtraFieldTestIds = {
extraFieldEditor: 'prom-editor-extra-field',
stepField: 'prom-editor-extra-field-step',
queryTypeField: 'prom-editor-extra-field-query-type',
};
const getStyles = (theme: GrafanaTheme2) => ({
queryTypeField: css({
marginRight: theme.spacing(0.5),
}),
});

View File

@@ -1,78 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.test.tsx
import { render, screen } from '@testing-library/react';
import { noop } from 'lodash';
import { CoreApp } from '@grafana/data';
import { type PrometheusDatasource } from '../datasource';
import { PromQueryEditorByApp } from './PromQueryEditorByApp';
import { alertingTestIds } from './PromQueryEditorForAlerting';
import { type Props } from './monaco-query-field/MonacoQueryFieldProps';
// the monaco-based editor uses lazy-loading and that does not work
// well with this test, and we do not need the monaco-related
// functionality in this test anyway, so we mock it out.
jest.mock('./monaco-query-field/MonacoQueryFieldLazy', () => {
const fakeQueryField = (props: Props) => {
return <input onBlur={(e) => props.onBlur(e.currentTarget.value)} data-testid={'dummy-code-input'} type={'text'} />;
};
return {
MonacoQueryFieldLazy: fakeQueryField,
};
});
function setup(app: CoreApp): { onRunQuery: jest.Mock } {
const dataSource = {
getPrometheusTime: jest.fn((date, roundup) => 123),
getQueryHints: jest.fn(() => []),
languageProvider: {
start: () => Promise.resolve([]),
retrieveMetrics: () => [],
},
} as unknown as PrometheusDatasource;
const onRunQuery = jest.fn();
render(
<PromQueryEditorByApp
app={app}
onChange={noop}
onRunQuery={onRunQuery}
datasource={dataSource}
query={{ refId: 'A', expr: '' }}
/>
);
return {
onRunQuery,
};
}
describe('PromQueryEditorByApp', () => {
it('should render simplified query editor for cloud alerting', async () => {
setup(CoreApp.CloudAlerting);
expect(await screen.findByTestId(alertingTestIds.editor)).toBeInTheDocument();
});
it('should render editor selector for unkown apps', () => {
setup(CoreApp.Unknown);
expect(screen.getByTestId('QueryEditorModeToggle')).toBeInTheDocument();
expect(screen.queryByTestId(alertingTestIds.editor)).toBeNull();
});
it('should render editor selector for explore', () => {
setup(CoreApp.Explore);
expect(screen.getByTestId('QueryEditorModeToggle')).toBeInTheDocument();
expect(screen.queryByTestId(alertingTestIds.editor)).toBeNull();
});
it('should render editor selector for dashboard', () => {
setup(CoreApp.Dashboard);
expect(screen.getByTestId('QueryEditorModeToggle')).toBeInTheDocument();
expect(screen.queryByTestId(alertingTestIds.editor)).toBeNull();
});
});

View File

@@ -1,22 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryEditorByApp.tsx
import { memo } from 'react';
import { CoreApp } from '@grafana/data';
import { PromQueryEditorSelector } from '../querybuilder/components/PromQueryEditorSelector';
import { PromQueryEditorForAlerting } from './PromQueryEditorForAlerting';
import { type PromQueryEditorProps } from './types';
function PromQueryEditorByAppBase(props: PromQueryEditorProps) {
const { app } = props;
switch (app) {
case CoreApp.CloudAlerting:
return <PromQueryEditorForAlerting {...props} />;
default:
return <PromQueryEditorSelector {...props} />;
}
}
export const PromQueryEditorByApp = memo(PromQueryEditorByAppBase);

View File

@@ -1,24 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryEditorForAlerting.tsx
import { PromQueryField } from './PromQueryField';
import { type PromQueryEditorProps } from './types';
export function PromQueryEditorForAlerting(props: PromQueryEditorProps) {
const { datasource, query, range, data, onChange, onRunQuery } = props;
return (
<PromQueryField
datasource={datasource}
query={query}
onRunQuery={onRunQuery}
onChange={onChange}
history={[]}
range={range}
data={data}
data-testid={alertingTestIds.editor}
/>
);
}
export const alertingTestIds = {
editor: 'prom-editor-cloud-alerting',
};

View File

@@ -1,148 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx
import { getByTestId, render, screen } from '@testing-library/react';
// @ts-ignore
import userEvent from '@testing-library/user-event';
import { CoreApp, type DataFrame, dateTime, LoadingState, type PanelData } from '@grafana/data';
import { type PrometheusDatasource } from '../datasource';
import * as queryHints from '../query_hints';
import { PromQueryField } from './PromQueryField';
import { type Props } from './monaco-query-field/MonacoQueryFieldProps';
// the monaco-based editor uses lazy-loading and that does not work
// well with this test, and we do not need the monaco-related
// functionality in this test anyway, so we mock it out.
jest.mock('./monaco-query-field/MonacoQueryFieldLazy', () => {
const fakeQueryField = (props: Props) => {
return <input onBlur={(e) => props.onBlur(e.currentTarget.value)} data-testid={'dummy-code-input'} type={'text'} />;
};
return {
MonacoQueryFieldLazy: fakeQueryField,
};
});
const defaultProps = {
datasource: {
languageProvider: {
start: () => Promise.resolve([]),
syntax: () => {},
// getLabelKeys: () => [],
retrieveMetrics: () => [],
},
} as unknown as PrometheusDatasource,
query: {
expr: '',
refId: '',
},
onRunQuery: () => {},
onChange: () => {},
history: [],
range: {
from: dateTime('2022-01-01T00:00:00Z'),
to: dateTime('2022-01-02T00:00:00Z'),
raw: {
from: 'now-1d',
to: 'now',
},
},
};
describe('PromQueryField', () => {
beforeAll(() => {
// @ts-ignore
window.getSelection = () => {};
});
beforeEach(() => {
jest.clearAllMocks();
});
it('renders metrics chooser regularly if lookups are not disabled in the datasource settings', async () => {
const queryField = render(<PromQueryField {...defaultProps} />);
// wait for component to render
await screen.findByRole('button');
expect(queryField.getAllByRole('button')).toHaveLength(1);
});
it('renders a disabled metrics chooser if lookups are disabled in datasource settings', async () => {
const props = defaultProps;
props.datasource.lookupsDisabled = true;
const queryField = render(<PromQueryField {...props} />);
// wait for component to render
await screen.findByRole('button');
const bcButton = queryField.getByRole('button');
expect(bcButton).toBeDisabled();
});
it('renders no metrics chooser if hidden by props', async () => {
const props = {
...defaultProps,
hideMetricsBrowser: true,
};
const queryField = render(<PromQueryField {...props} />);
// wait for component to render
await screen.findByTestId('dummy-code-input');
expect(queryField.queryByRole('button')).not.toBeInTheDocument();
});
it('renders an initial hint if no data and initial hint provided', async () => {
const props = defaultProps;
props.datasource.lookupsDisabled = true;
jest.spyOn(queryHints, 'getInitHints').mockReturnValue([{ label: 'Initial hint', type: 'INFO' }]);
render(<PromQueryField {...props} />);
// wait for component to render
await screen.findByRole('button');
expect(screen.getByText('Initial hint')).toBeInTheDocument();
});
it('renders query hint if data, query hint and initial hint provided', async () => {
const props = defaultProps;
props.datasource.lookupsDisabled = true;
props.datasource.getQueryHints = () => [{ label: 'Query hint', type: 'INFO' }];
render(
<PromQueryField
{...props}
data={
{
series: [{ name: 'test name' }] as DataFrame[],
state: LoadingState.Done,
} as PanelData
}
/>
);
// wait for component to render
await screen.findByRole('button');
expect(screen.getByText('Query hint')).toBeInTheDocument();
expect(screen.queryByText('Initial hint')).not.toBeInTheDocument();
});
it('should not run query onBlur', async () => {
const onRunQuery = jest.fn();
const { container } = render(<PromQueryField {...defaultProps} app={CoreApp.Explore} onRunQuery={onRunQuery} />);
// wait for component to rerender
await screen.findByRole('button');
const input = getByTestId(container, 'dummy-code-input');
expect(input).toBeInTheDocument();
await userEvent.type(input, 'metric');
// blur element
await userEvent.click(document.body);
expect(onRunQuery).not.toHaveBeenCalled();
});
});

View File

@@ -1,183 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
import { css, cx } from '@emotion/css';
import { type ReactNode, useCallback, useEffect, useState } from 'react';
import {
type DataFrame,
getDefaultTimeRange,
isDataFrame,
type QueryEditorProps,
type QueryHint,
toLegacyResponseData,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { clearButtonStyles, Icon, useTheme2 } from '@grafana/ui';
import { type PrometheusDatasource } from '../datasource';
import { getInitHints } from '../query_hints';
import { type PromOptions, type PromQuery } from '../types';
import { MetricsBrowser } from './metrics-browser/MetricsBrowser';
import { MetricsBrowserProvider } from './metrics-browser/MetricsBrowserContext';
import { MonacoQueryFieldWrapper } from './monaco-query-field/MonacoQueryFieldWrapper';
interface PromQueryFieldProps extends QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions> {
ExtraFieldElement?: ReactNode;
hideMetricsBrowser?: boolean;
'data-testid'?: string;
}
export const PromQueryField = (props: PromQueryFieldProps) => {
const {
app,
datasource,
datasource: { languageProvider },
query,
ExtraFieldElement,
history = [],
data,
range,
onChange,
onRunQuery,
hideMetricsBrowser = false,
} = props;
const theme = useTheme2();
const [hint, setHint] = useState<QueryHint | null>(null);
const [labelBrowserVisible, setLabelBrowserVisible] = useState(false);
const refreshHint = useCallback(
(series: DataFrame[]) => {
const initHints = getInitHints(datasource);
const initHint = initHints[0] ?? null;
// If no data or empty series, use default hint
if (!data?.series?.length) {
setHint(initHint);
return;
}
const result = isDataFrame(series[0]) ? series.map(toLegacyResponseData) : series;
const queryHints = datasource.getQueryHints(query, result);
let queryHint = queryHints.length > 0 ? queryHints[0] : null;
setHint(queryHint ?? initHint);
},
[data, datasource, query]
);
useEffect(() => {
refreshHint(data?.series ?? []);
}, [data?.series, refreshHint]);
const onChangeQuery = (value: string, override?: boolean) => {
if (!onChange) {
return;
}
// Send text change to parent
const nextQuery: PromQuery = { ...query, expr: value };
onChange(nextQuery);
if (override && onRunQuery) {
onRunQuery();
}
};
const onChangeLabelBrowser = (selector: string) => {
onChangeQuery(selector, true);
setLabelBrowserVisible(false);
};
const onClickChooserButton = () => {
setLabelBrowserVisible(!labelBrowserVisible);
reportInteraction('user_grafana_prometheus_metrics_browser_clicked', {
editorMode: labelBrowserVisible ? 'metricViewClosed' : 'metricViewOpen',
app: app ?? '',
});
};
const onClickHintFix = () => {
if (hint?.fix?.action) {
onChange(datasource.modifyQuery(query, hint.fix.action));
}
onRunQuery();
};
return (
<>
<div
className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1"
data-testid={props['data-testid']}
>
{!hideMetricsBrowser && (
<button
className="gf-form-label query-keyword pointer"
onClick={onClickChooserButton}
disabled={datasource.lookupsDisabled}
type="button"
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton}
>
{datasource.lookupsDisabled ? (
<Trans i18nKey="grafana-prometheus.metrics-browser.disabled-label">(Disabled)</Trans>
) : (
<Trans i18nKey="grafana-prometheus.metrics-browser.enabled-label">Metrics browser</Trans>
)}
<Icon name={labelBrowserVisible ? 'angle-down' : 'angle-right'} />
</button>
)}
<div className="flex-grow-1 min-width-15">
<MonacoQueryFieldWrapper
languageProvider={languageProvider}
history={history}
onChange={onChangeQuery}
onRunQuery={onRunQuery}
initialValue={query.expr ?? ''}
placeholder={t(
'grafana-prometheus.components.prom-query-field.placeholder-enter-a-prom-ql-query',
'Enter a PromQL query…'
)}
datasource={datasource}
timeRange={range ?? getDefaultTimeRange()}
/>
</div>
</div>
{labelBrowserVisible && (
<div>
<MetricsBrowserProvider
timeRange={range ?? getDefaultTimeRange()}
languageProvider={languageProvider}
onChange={onChangeLabelBrowser}
>
<MetricsBrowser />
</MetricsBrowserProvider>
</div>
)}
{ExtraFieldElement}
{hint ? (
<div
className={css({
flexBasis: '100%',
})}
>
<div className="text-warning">
{hint.label}{' '}
{hint.fix ? (
<button
type="button"
className={cx(clearButtonStyles(theme), 'text-link', 'muted')}
onClick={onClickHintFix}
>
{hint.fix.label}
</button>
) : null}
</div>
</div>
) : null}
</>
);
};

View File

@@ -1,378 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { select } from 'react-select-event';
import { selectors } from '@grafana/e2e-selectors';
import { type PrometheusDatasource } from '../datasource';
import { type PrometheusLanguageProviderInterface } from '../language_provider';
import { migrateVariableEditorBackToVariableSupport } from '../migrations/variableMigration';
import { type PromVariableQuery, PromVariableQueryType, type StandardPromVariableQuery } from '../types';
import { PromVariableQueryEditor, type Props, variableMigration } from './VariableQueryEditor';
const refId = 'PrometheusVariableQueryEditor-VariableQuery';
describe('PromVariableQueryEditor', () => {
let props: Props;
test('Migrates from standard variable support to custom variable query', () => {
const query: StandardPromVariableQuery = {
query: 'label_names()',
refId: 'StandardVariableQuery',
};
const migration: PromVariableQuery = variableMigration(query);
const expected: PromVariableQuery = {
qryType: PromVariableQueryType.LabelNames,
refId: 'PrometheusDatasource-VariableQuery',
};
expect(migration).toEqual(expected);
});
test('Allows for use of variables to interpolate label names in the label values query type.', () => {
const query: StandardPromVariableQuery = {
query: 'label_values($label_name)',
refId: 'StandardVariableQuery',
};
const migration: PromVariableQuery = variableMigration(query);
const expected: PromVariableQuery = {
qryType: PromVariableQueryType.LabelValues,
label: '$label_name',
refId: 'PrometheusDatasource-VariableQuery',
};
expect(migration).toEqual(expected);
});
test('Migrates from jsonnet grafana as code variable to custom variable query', () => {
const query = 'label_names()';
const migration: PromVariableQuery = variableMigration(query);
const expected: PromVariableQuery = {
qryType: PromVariableQueryType.LabelNames,
refId: 'PrometheusDatasource-VariableQuery',
};
expect(migration).toEqual(expected);
});
test('Migrates label filters to the query object for label_values()', () => {
const query: StandardPromVariableQuery = {
query: 'label_values(metric{label="value"},name)',
refId: 'StandardVariableQuery',
};
const migration: PromVariableQuery = variableMigration(query);
const expected: PromVariableQuery = {
qryType: PromVariableQueryType.LabelValues,
label: 'name',
metric: 'metric',
labelFilters: [
{
label: 'label',
op: '=',
value: 'value',
},
],
refId: 'PrometheusDatasource-VariableQuery',
};
expect(migration).toEqual(expected);
});
test('Migrates a query object with label filters to an expression correctly', () => {
const query: PromVariableQuery = {
qryType: PromVariableQueryType.LabelValues,
label: 'name',
metric: 'metric',
labelFilters: [
{
label: 'label',
op: '=',
value: 'value',
},
],
refId: 'PrometheusDatasource-VariableQuery',
};
const migration: string = migrateVariableEditorBackToVariableSupport(query);
const expected = 'label_values(metric{label="value"},name)';
expect(migration).toEqual(expected);
});
test('Migrates a query object with no metric and only label filters to an expression correctly', () => {
const query: PromVariableQuery = {
qryType: PromVariableQueryType.LabelValues,
label: 'name',
labelFilters: [
{
label: 'label',
op: '=',
value: 'value',
},
],
refId: 'PrometheusDatasource-VariableQuery',
};
const migration: string = migrateVariableEditorBackToVariableSupport(query);
const expected = 'label_values({label="value"},name)';
expect(migration).toEqual(expected);
});
beforeEach(() => {
props = {
datasource: {
hasLabelsMatchAPISupport: () => true,
languageProvider: {
start: () => Promise.resolve([]),
queryLabelKeys: jest.fn().mockResolvedValue(['those']),
queryLabelValues: jest.fn().mockResolvedValue(['that']),
} as Partial<PrometheusLanguageProviderInterface>,
getTagKeys: jest.fn().mockResolvedValue([{ text: 'this', value: 'this', label: 'this' }]),
getVariables: jest.fn().mockReturnValue([]),
metricFindQuery: jest.fn().mockResolvedValue([
{
text: 'that',
value: 'that',
label: 'that',
},
]),
} as Partial<PrometheusDatasource> as PrometheusDatasource,
query: {
refId: 'test',
query: 'label_names()',
},
onRunQuery: () => {},
onChange: () => {},
history: [],
};
});
test('Displays a group of function options', async () => {
render(<PromVariableQueryEditor {...props} />);
const select = screen.getByLabelText('Query type').parentElement!;
await userEvent.click(select);
await waitFor(() => expect(screen.getAllByText('Label names')).toHaveLength(2));
await waitFor(() => expect(screen.getByText('Label values')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Metrics')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Query result')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Series query')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Classic query')).toBeInTheDocument());
});
test('Calls onChange for label_names(match) query', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: '',
match: 'that',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label names', { container: document.body }));
expect(onChange).toHaveBeenCalledWith({
query: 'label_names(that)',
refId,
qryType: 0,
});
});
test('Calls onChange for label_names, label_values, metrics, query result and classic query.', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: '',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label names', { container: document.body }));
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label values', { container: document.body }));
await waitFor(() => select(screen.getByLabelText('Query type'), 'Metrics', { container: document.body }));
await waitFor(() => select(screen.getByLabelText('Query type'), 'Query result', { container: document.body }));
await waitFor(() => select(screen.getByLabelText('Query type'), 'Classic query', { container: document.body }));
expect(onChange).toHaveBeenCalledTimes(5);
});
test('Does not call onChange for series query', async () => {
const onChange = jest.fn();
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await waitFor(() => select(screen.getByLabelText('Query type'), 'Series query', { container: document.body }));
expect(onChange).not.toHaveBeenCalled();
});
test('Calls onChange for metrics() after input', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: 'label_names()',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await waitFor(() => select(screen.getByLabelText('Query type'), 'Metrics', { container: document.body }));
const metricInput = screen.getByLabelText('Metric selector');
await userEvent.type(metricInput, 'a');
const queryType = screen.getByLabelText('Query type');
// click elsewhere to trigger the onBlur
await userEvent.click(queryType);
await waitFor(() =>
expect(onChange).toHaveBeenCalledWith({
query: 'metrics(a)',
refId,
qryType: 2,
})
);
});
test('Calls onChange for label_values() after selecting label', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: 'label_names()',
qryType: 0,
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label values', { container: document.body }));
const labelSelect = screen.getByTestId(
selectors.components.DataSource.Prometheus.variableQueryEditor.labelValues.labelSelect
);
await userEvent.type(labelSelect, 'this');
await waitFor(() => select(labelSelect, 'this', { container: document.body }));
//display label in label select
await waitFor(() => expect(screen.getByText('this')).toBeInTheDocument());
await waitFor(() =>
expect(onChange).toHaveBeenCalledWith({
query: 'label_values(this)',
refId,
qryType: 1,
})
);
});
test('Calls onChange for label_values() after selecting metric', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: 'label_names()',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label values', { container: document.body }));
const labelSelect = screen.getByTestId(
selectors.components.DataSource.Prometheus.variableQueryEditor.labelValues.labelSelect
);
await userEvent.type(labelSelect, 'this');
await waitFor(() => select(labelSelect, 'this', { container: document.body }));
const combobox = screen.getByPlaceholderText('Select metric');
await userEvent.type(combobox, 'that');
await userEvent.keyboard('{Enter}');
await waitFor(() =>
expect(onChange).toHaveBeenCalledWith({
query: 'label_values(that,this)',
refId,
qryType: 1,
})
);
});
test('Calls onChange for query_result() with argument onBlur', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: 'query_result(a)',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
const labelSelect = screen.getByLabelText('Prometheus Query');
await userEvent.click(labelSelect);
const functionSelect = screen.getByLabelText('Query type').parentElement!;
await userEvent.click(functionSelect);
expect(onChange).toHaveBeenCalledWith({
query: 'query_result(a)',
refId,
qryType: 3,
});
});
test('Calls onChange for Match[] series with argument onBlur', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
query: '{a: "example"}',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
const labelSelect = screen.getByLabelText('Series Query');
await userEvent.click(labelSelect);
const functionSelect = screen.getByLabelText('Query type').parentElement!;
await userEvent.click(functionSelect);
expect(onChange).toHaveBeenCalledWith({
query: '{a: "example"}',
refId,
qryType: 4,
});
});
test('Calls onChange for classic query onBlur', async () => {
const onChange = jest.fn();
props.query = {
refId: 'test',
qryType: 5,
query: 'label_values(instance)',
};
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
const labelSelect = screen.getByLabelText('Classic Query');
await userEvent.click(labelSelect);
const functionSelect = screen.getByLabelText('Query type').parentElement!;
await userEvent.click(functionSelect);
expect(onChange).toHaveBeenCalledWith({
query: 'label_values(instance)',
refId,
qryType: 5,
});
});
});

View File

@@ -1,541 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx
import debounce from 'debounce-promise';
import { type FormEvent, useCallback, useEffect, useState } from 'react';
import { getDefaultTimeRange, type QueryEditorProps, type SelectableValue, toOption } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { AsyncSelect, InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui';
import { type PrometheusDatasource } from '../datasource';
import { truncateResult } from '../language_utils';
import {
migrateVariableEditorBackToVariableSupport,
migrateVariableQueryToEditor,
} from '../migrations/variableMigration';
import { MetricsLabelsSection } from '../querybuilder/components/MetricsLabelsSection';
import { promQueryModeller } from '../querybuilder/shared/modeller_instance';
import { type QueryBuilderLabelFilter } from '../querybuilder/shared/types';
import { type PromVisualQuery } from '../querybuilder/types';
import {
type PromOptions,
type PromQuery,
type PromVariableQuery,
PromVariableQueryType as QueryType,
type StandardPromVariableQuery,
} from '../types';
const variableOptions = [
{ label: 'Label names', value: QueryType.LabelNames },
{ label: 'Label values', value: QueryType.LabelValues },
{ label: 'Metrics', value: QueryType.MetricNames },
{ label: 'Query result', value: QueryType.VarQueryResult },
{ label: 'Series query', value: QueryType.SeriesQuery },
{ label: 'Classic query', value: QueryType.ClassicQuery },
];
export type Props = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions, PromVariableQuery>;
const refId = 'PrometheusVariableQueryEditor-VariableQuery';
export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: Props) => {
// to select the query type, i.e. label_names, label_values, etc.
const [qryType, setQryType] = useState<number | undefined>(undefined);
// list of variables for each function
const [label, setLabel] = useState('');
const [labelNamesMatch, setLabelNamesMatch] = useState('');
// metric is used for both label_values() and metric()
// label_values() metric requires a whole/complete metric
// metric() is expected to be a part of a metric string
const [metric, setMetric] = useState('');
// varQuery is a whole query, can include math/rates/etc
const [varQuery, setVarQuery] = useState('');
// seriesQuery is only a whole
const [seriesQuery, setSeriesQuery] = useState('');
// the original variable query implementation, e.g. label_value(metric, label_name)
const [classicQuery, setClassicQuery] = useState('');
// list of label names for label_values(), /api/v1/labels, contains the same results as label_names() function
const [truncatedLabelOptions, setTruncatedLabelOptions] = useState<Array<SelectableValue<string>>>([]);
const [allLabelOptions, setAllLabelOptions] = useState<Array<SelectableValue<string>>>([]);
/**
* Set the both allLabels and truncatedLabels
*
* @param names
* @param variables
*/
function setLabels(names: SelectableValue[], variables: SelectableValue[]) {
setAllLabelOptions([...variables, ...names]);
const truncatedNames = truncateResult(names);
setTruncatedLabelOptions([...variables, ...truncatedNames]);
}
// label filters have been added as a filter for metrics in label values query type
const [labelFilters, setLabelFilters] = useState<QueryBuilderLabelFilter[]>([]);
useEffect(() => {
if (!query) {
return;
}
if (query.qryType === QueryType.ClassicQuery) {
setQryType(query.qryType);
setClassicQuery(query.query ?? '');
} else {
// 1. Changing from standard to custom variable editor changes the string attr from expr to query
// 2. jsonnet grafana as code passes a variable as a string
const variableQuery = variableMigration(query);
setLabelNamesMatch(variableQuery.match ?? '');
setQryType(variableQuery.qryType);
setLabel(variableQuery.label ?? '');
setMetric(variableQuery.metric ?? '');
setLabelFilters(variableQuery.labelFilters ?? []);
setVarQuery(variableQuery.varQuery ?? '');
setSeriesQuery(variableQuery.seriesQuery ?? '');
setClassicQuery(variableQuery.classicQuery ?? '');
}
}, [query]);
// set the label names options for the label values var query
useEffect(() => {
if (qryType !== QueryType.LabelValues) {
return;
}
const variables = datasource.getVariables().map((variable: string) => ({ label: variable, value: variable }));
let timeRange = range;
if (!timeRange) {
timeRange = getDefaultTimeRange();
}
if (!metric) {
// get all the labels
datasource.getTagKeys({ timeRange, filters: [] }).then((labelNames: Array<{ text: string }>) => {
const names = labelNames.map(({ text }) => ({ label: text, value: text }));
setLabels(names, variables);
});
} else {
// fetch the labels filtered by the metric
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
const labelToConsider = [{ label: '__name__', op: '=', value: metric }];
const expr = promQueryModeller.renderLabels(labelToConsider);
datasource.languageProvider.queryLabelKeys(timeRange, expr).then((labelNames: string[]) => {
const names = labelNames.map((value) => ({ label: value, value: value }));
setLabels(names, variables);
});
}
}, [datasource, qryType, metric, range]);
const onChangeWithVariableString = (
updateVar: { [key: string]: QueryType | string },
updLabelFilters?: QueryBuilderLabelFilter[]
) => {
const queryVar = {
qryType,
label,
metric,
match: labelNamesMatch,
varQuery,
seriesQuery,
classicQuery,
refId: 'PrometheusVariableQueryEditor-VariableQuery',
};
let updateLabelFilters = updLabelFilters ? { labelFilters: updLabelFilters } : { labelFilters: labelFilters };
const updatedVar = { ...queryVar, ...updateVar, ...updateLabelFilters };
const queryString = migrateVariableEditorBackToVariableSupport(updatedVar);
// setting query.query property allows for update of variable definition
onChange({
query: queryString,
qryType: updatedVar.qryType,
refId,
});
};
/** Call onchange for label names query type change */
const onQueryTypeChange = (newType: SelectableValue<QueryType>) => {
setQryType(newType.value);
if (newType.value !== QueryType.SeriesQuery) {
onChangeWithVariableString({ qryType: newType.value ?? 0 });
}
};
/** Call onchange for label select when query type is label values */
const onLabelChange = (newLabel: SelectableValue<string>) => {
const newLabelvalue = newLabel && newLabel.value ? newLabel.value : '';
setLabel(newLabelvalue);
if (qryType === QueryType.LabelValues && newLabelvalue) {
onChangeWithVariableString({ label: newLabelvalue });
}
};
/**
* Call onChange for MetricsLabels component change for label values query type
* if there is a label (required) and
* if the labels or metric are updated.
*/
const metricsLabelsChange = (update: PromVisualQuery) => {
setMetric(update.metric);
setLabelFilters(update.labels);
const updMetric = update.metric;
const updLabelFilters = update.labels ?? [];
if (qryType === QueryType.LabelValues && label && (updMetric || updLabelFilters)) {
onChangeWithVariableString({ qryType, metric: updMetric }, updLabelFilters);
}
};
const onLabelNamesMatchChange = (regex: string) => {
if (qryType === QueryType.LabelNames) {
onChangeWithVariableString({ qryType, match: regex });
}
};
/**
* Call onchange for metric change if metrics names (regex) query type
* Debounce this because to not call the API for every keystroke.
*/
const onMetricChange = (value: string) => {
if (qryType === QueryType.MetricNames && value) {
onChangeWithVariableString({ metric: value });
}
};
/**
* Do not call onchange for variable query result when query type is var query result
* because the query may not be finished typing and an error is returned
* for incorrectly formatted series. Call onchange for blur instead.
*/
const onVarQueryChange = (e: FormEvent<HTMLTextAreaElement>) => {
setVarQuery(e.currentTarget.value);
};
/**
* Do not call onchange for seriesQuery when query type is series query
* because the series may not be finished typing and an error is returned
* for incorrectly formatted series. Call onchange for blur instead.
*/
const onSeriesQueryChange = (e: FormEvent<HTMLInputElement>) => {
setSeriesQuery(e.currentTarget.value);
};
const onClassicQueryChange = (e: FormEvent<HTMLInputElement>) => {
setClassicQuery(e.currentTarget.value);
};
const promVisualQuery = useCallback(() => {
return { metric: metric, labels: labelFilters, operations: [] };
}, [metric, labelFilters]);
/**
* Debounce a search through all the labels possible and truncate by .
*/
const labelNamesSearch = debounce((query: string) => {
// we limit the select to show 1000 options,
// but we still search through all the possible options
const results = allLabelOptions.filter((label) => {
return label.value?.includes(query);
});
return truncateResult(results);
}, 300);
return (
<>
<InlineFieldRow>
<InlineField
label={t('grafana-prometheus.components.prom-variable-query-editor.label-query-type', 'Query type')}
labelWidth={20}
tooltip={
<div>
<Trans i18nKey="grafana-prometheus.components.prom-variable-query-editor.tooltip-query-type">
The Prometheus data source plugin provides the following query types for template variables.
</Trans>
</div>
}
>
<Select
placeholder={t(
'grafana-prometheus.components.prom-variable-query-editor.placeholder-select-query-type',
'Select query type'
)}
aria-label={t(
'grafana-prometheus.components.prom-variable-query-editor.aria-label-query-type',
'Query type'
)}
onChange={onQueryTypeChange}
value={qryType}
options={variableOptions}
width={25}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.queryType}
/>
</InlineField>
</InlineFieldRow>
{qryType === QueryType.LabelValues && (
<>
<InlineFieldRow>
<InlineField
label={t('grafana-prometheus.components.prom-variable-query-editor.label-label', 'Label')}
labelWidth={20}
required
aria-labelledby="label-select"
tooltip={
<div>
<Trans i18nKey="grafana-prometheus.components.prom-variable-query-editor.tooltip-label">
Returns a list of label values for the label name in all metrics unless the metric is specified.
</Trans>
</div>
}
>
<AsyncSelect
onChange={onLabelChange}
value={label ? toOption(label) : null}
defaultOptions={truncatedLabelOptions}
width={25}
allowCustomValue
isClearable={true}
loadOptions={labelNamesSearch}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.labelValues.labelSelect}
/>
</InlineField>
</InlineFieldRow>
{/* Used to select an optional metric with optional label filters */}
<MetricsLabelsSection
query={promVisualQuery()}
datasource={datasource}
onChange={metricsLabelsChange}
variableEditor={true}
timeRange={range ?? getDefaultTimeRange()}
/>
</>
)}
{qryType === QueryType.LabelNames && (
<InlineFieldRow>
<InlineField
label={t('grafana-prometheus.components.prom-variable-query-editor.label-metric-regex', 'Metric regex')}
labelWidth={20}
aria-labelledby="Metric regex"
tooltip={
<div>
<Trans i18nKey="grafana-prometheus.components.prom-variable-query-editor.tooltip-metric-regex">
Returns a list of label names, optionally filtering by specified metric regex.
</Trans>
</div>
}
>
<Input
type="text"
aria-label={t(
'grafana-prometheus.components.prom-variable-query-editor.aria-label-metric-regex',
'Metric regex'
)}
placeholder={t(
'grafana-prometheus.components.prom-variable-query-editor.placeholder-metric-regex',
'Metric regex'
)}
value={labelNamesMatch}
onBlur={(event) => {
setLabelNamesMatch(event.currentTarget.value);
onLabelNamesMatchChange(event.currentTarget.value);
}}
onChange={(e) => {
setLabelNamesMatch(e.currentTarget.value);
}}
width={25}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.labelnames.metricRegex}
/>
</InlineField>
</InlineFieldRow>
)}
{qryType === QueryType.MetricNames && (
<InlineFieldRow>
<InlineField
label={t('grafana-prometheus.components.prom-variable-query-editor.label-metric-regex', 'Metric regex')}
labelWidth={20}
aria-labelledby="Metric selector"
tooltip={
<div>
<Trans i18nKey="grafana-prometheus.components.prom-variable-query-editor.returns-metrics-matching-specified-metric-regex">
Returns a list of metrics matching the specified metric regex.
</Trans>
</div>
}
>
<Input
type="text"
aria-label={t(
'grafana-prometheus.components.prom-variable-query-editor.aria-label-metric-selector',
'Metric selector'
)}
placeholder={t(
'grafana-prometheus.components.prom-variable-query-editor.placeholder-metric-regex',
'Metric regex'
)}
value={metric}
onChange={(e) => {
setMetric(e.currentTarget.value);
}}
onBlur={(e) => {
setMetric(e.currentTarget.value);
onMetricChange(e.currentTarget.value);
}}
width={25}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.metricNames.metricRegex}
/>
</InlineField>
</InlineFieldRow>
)}
{qryType === QueryType.VarQueryResult && (
<InlineFieldRow>
<InlineField
label={t('grafana-prometheus.components.prom-variable-query-editor.label-query', 'Query')}
labelWidth={20}
tooltip={
<div>
<Trans
i18nKey="grafana-prometheus.components.prom-variable-query-editor.tooltip-query"
values={{ exampleQuery: 'sum(go_goroutines)' }}
>
Returns a list of Prometheus query results for the query. This can include Prometheus functions, i.e.
{'{{exampleQuery}}'}.
</Trans>
</div>
}
>
<TextArea
type="text"
aria-label={t(
'grafana-prometheus.components.prom-variable-query-editor.aria-label-prometheus-query',
'Prometheus Query'
)}
placeholder={t(
'grafana-prometheus.components.prom-variable-query-editor.placeholder-prometheus-query',
'Prometheus Query'
)}
value={varQuery}
onChange={onVarQueryChange}
onBlur={() => {
if (qryType === QueryType.VarQueryResult && varQuery) {
onChangeWithVariableString({ qryType });
}
}}
cols={100}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.varQueryResult}
/>
</InlineField>
</InlineFieldRow>
)}
{qryType === QueryType.SeriesQuery && (
<InlineFieldRow>
<InlineField
label={t('grafana-prometheus.components.prom-variable-query-editor.label-series-query', 'Series Query')}
labelWidth={20}
tooltip={
<div>
<Trans
i18nKey="grafana-prometheus.components.prom-variable-query-editor.tooltip-series-query"
values={{
example1: 'go_goroutines{instance="localhost:9090"}',
example2: 'go_goroutines',
example3: '{instance="localhost:9090"}',
}}
>
Enter a metric with labels, only a metric or only labels, i.e.
{'{{example1}}'}, {'{{example2}}'}, or {'{{example3}}'}. Returns a list of time series associated with
the entered data.
</Trans>
</div>
}
>
<Input
type="text"
aria-label={t(
'grafana-prometheus.components.prom-variable-query-editor.aria-label-series-query',
'Series Query'
)}
placeholder={t(
'grafana-prometheus.components.prom-variable-query-editor.placeholder-series-query',
'Series Query'
)}
value={seriesQuery}
onChange={onSeriesQueryChange}
onBlur={() => {
if (qryType === QueryType.SeriesQuery && seriesQuery) {
onChangeWithVariableString({ qryType });
}
}}
width={100}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.seriesQuery}
/>
</InlineField>
</InlineFieldRow>
)}
{qryType === QueryType.ClassicQuery && (
<InlineFieldRow>
<InlineField
label={t('grafana-prometheus.components.prom-variable-query-editor.label-classic-query', 'Classic Query')}
labelWidth={20}
tooltip={
<div>
<Trans
i18nKey="grafana-prometheus.components.prom-variable-query-editor.tooltip-classic-query"
values={{
exampleQuery: 'label_values(metric, label)',
}}
>
The original implementation of the Prometheus variable query editor. Enter a string with the correct
query type and parameters as described in these docs. For example, {'{{exampleQuery}}'}.
</Trans>
</div>
}
>
<Input
type="text"
aria-label={t(
'grafana-prometheus.components.prom-variable-query-editor.aria-label-classic-query',
'Classic Query'
)}
placeholder={t(
'grafana-prometheus.components.prom-variable-query-editor.placeholder-classic-query',
'Classic Query'
)}
value={classicQuery}
onChange={onClassicQueryChange}
onBlur={() => {
if (qryType === QueryType.ClassicQuery && classicQuery) {
onChangeWithVariableString({ qryType });
}
}}
width={100}
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.classicQuery}
/>
</InlineField>
</InlineFieldRow>
)}
</>
);
};
export function variableMigration(query: string | PromVariableQuery | StandardPromVariableQuery): PromVariableQuery {
if (typeof query === 'string') {
return migrateVariableQueryToEditor(query);
} else if (query.query) {
return migrateVariableQueryToEditor(query.query);
} else {
return query;
}
}

View File

@@ -1,32 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/core/utils/CancelablePromise.ts
// https://github.com/facebook/react/issues/5465
export interface CancelablePromise<T> {
promise: Promise<T>;
cancel: () => void;
}
interface CancelablePromiseRejection {
isCanceled: boolean;
}
export function isCancelablePromiseRejection(promise: unknown): promise is CancelablePromiseRejection {
return typeof promise === 'object' && promise !== null && 'isCanceled' in promise;
}
export const makePromiseCancelable = <T>(promise: Promise<T>): CancelablePromise<T> => {
let hasCanceled_ = false;
const wrappedPromise = new Promise<T>((resolve, reject) => {
const canceledPromiseRejection: CancelablePromiseRejection = { isCanceled: true };
promise.then((val) => (hasCanceled_ ? reject(canceledPromiseRejection) : resolve(val)));
promise.catch((error) => (hasCanceled_ ? reject(canceledPromiseRejection) : reject(error)));
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};

View File

@@ -1,73 +0,0 @@
import { useMemo, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { BrowserLabel as PromLabel, Input, Label, useStyles2, Spinner } from '@grafana/ui';
import { METRIC_LABEL } from '../../constants';
import { useMetricsBrowser } from './MetricsBrowserContext';
import { getStylesLabelSelector, getStylesMetricsBrowser } from './styles';
export function LabelSelector() {
const styles = useStyles2(getStylesLabelSelector);
const sharedStyles = useStyles2(getStylesMetricsBrowser);
const [labelSearchTerm, setLabelSearchTerm] = useState('');
const { labelKeys, isLoadingLabelKeys, selectedLabelKeys, onLabelKeyClick } = useMetricsBrowser();
const filteredLabelKeys = useMemo(() => {
return labelKeys.filter(
(lk) => lk !== METRIC_LABEL && (selectedLabelKeys.includes(lk) || lk.includes(labelSearchTerm))
);
}, [labelKeys, labelSearchTerm, selectedLabelKeys]);
return (
<div className={styles.section}>
<Label
description={t(
'grafana-prometheus.components.label-selector.description-select-labels',
'Once label values are selected, only possible label combinations are shown.'
)}
>
<Trans i18nKey="grafana-prometheus.components.label-selector.select-labels-to-search-in">
2. Select labels to search in
</Trans>
</Label>
<div>
<Input
onChange={(e) => setLabelSearchTerm(e.currentTarget.value)}
aria-label={t(
'grafana-prometheus.components.label-selector.aria-label-filter-expression-for-label',
'Filter expression for label'
)}
value={labelSearchTerm}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelNamesFilter}
/>
</div>
{/* Using fixed height here to prevent jumpy layout */}
{isLoadingLabelKeys ? (
<div className={sharedStyles.spinner}>
<Spinner size="xl" />
</div>
) : (
<div className={styles.list} style={{ height: 120 }}>
{filteredLabelKeys.map((label) => (
<PromLabel
key={label}
name={label}
active={selectedLabelKeys.includes(label)}
hidden={false}
facets={undefined}
onClick={(name: string) => {
// Resetting search to prevent empty results
setLabelSearchTerm('');
onLabelKeyClick(name);
}}
searchTerm={labelSearchTerm}
/>
))}
</div>
)}
</div>
);
}

View File

@@ -1,100 +0,0 @@
import { useMemo, useState } from 'react';
import { FixedSizeList } from 'react-window';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { BrowserLabel as PromLabel, Input, Label, useStyles2 } from '@grafana/ui';
import { LIST_ITEM_SIZE } from '../../constants';
import { useMetricsBrowser } from './MetricsBrowserContext';
import { getStylesMetricSelector } from './styles';
export function MetricSelector() {
const styles = useStyles2(getStylesMetricSelector);
const [metricSearchTerm, setMetricSearchTerm] = useState('');
const { metrics, selectedMetric, seriesLimit, setSeriesLimit, onMetricClick } = useMetricsBrowser();
const filteredMetrics = useMemo(() => {
return metrics.filter((m) => m.name === selectedMetric || m.name.includes(metricSearchTerm));
}, [metrics, selectedMetric, metricSearchTerm]);
return (
<div>
<div className={styles.section}>
<Label
description={t(
'grafana-prometheus.components.metric-selector.label-select-metric',
'Once a metric is selected only possible labels are shown. Labels are limited by the series limit below.'
)}
>
<Trans i18nKey="grafana-prometheus.components.metric-selector.select-a-metric">1. Select a metric</Trans>
</Label>
<div>
<Input
onChange={(e) => setMetricSearchTerm(e.currentTarget.value)}
aria-label={t(
'grafana-prometheus.components.metric-selector.aria-label-filter-expression-for-metric',
'Filter expression for metric'
)}
value={metricSearchTerm}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric}
/>
</div>
<Label
description={t(
'grafana-prometheus.components.metric-selector.description-series-limit',
'The limit applies to all metrics, labels, and values. Leave the field empty to use the default limit. Set to 0 to disable the limit and fetch everything — this may cause performance issues.'
)}
>
<Trans i18nKey="grafana-prometheus.components.metric-selector.series-limit">Series limit</Trans>
</Label>
<div>
<Input
onChange={(e) => setSeriesLimit(parseInt(e.currentTarget.value.trim(), 10))}
aria-label={t(
'grafana-prometheus.components.metric-selector.aria-label-limit-results-from-series-endpoint',
'Limit results from series endpoint'
)}
value={seriesLimit}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.seriesLimit}
/>
</div>
<div
role="list"
className={styles.valueListWrapper}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList}
>
<FixedSizeList
height={Math.min(450, filteredMetrics.length * LIST_ITEM_SIZE)}
itemCount={filteredMetrics.length}
itemSize={LIST_ITEM_SIZE}
itemKey={(i) => filteredMetrics[i].name}
width={300}
className={styles.valueList}
>
{({ index, style }) => {
const metric = filteredMetrics[index];
return (
<div style={style}>
<PromLabel
name={metric.name}
value={metric.name}
title={metric.details}
active={metric.name === selectedMetric}
onClick={(name: string, value: string | undefined) => {
// Resetting search to prevent empty results
setMetricSearchTerm('');
onMetricClick(name);
}}
searchTerm={metricSearchTerm}
/>
</div>
);
}}
</FixedSizeList>
</div>
</div>
</div>
);
}

View File

@@ -1,27 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx
import { Stack, useStyles2 } from '@grafana/ui';
import { LabelSelector } from './LabelSelector';
import { MetricSelector } from './MetricSelector';
import { SelectorActions } from './SelectorActions';
import { ValueSelector } from './ValueSelector';
import { getStylesMetricsBrowser } from './styles';
export const MetricsBrowser = () => {
const styles = useStyles2(getStylesMetricsBrowser);
return (
<div className={styles.wrapper}>
<Stack gap={3}>
<MetricSelector />
<div>
<LabelSelector />
<ValueSelector />
</div>
</Stack>
<SelectorActions />
</div>
);
};

View File

@@ -1,432 +0,0 @@
import { render, renderHook, screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { type ReactNode } from 'react';
import { type TimeRange } from '@grafana/data';
import { DEFAULT_SERIES_LIMIT, LAST_USED_LABELS_KEY, METRIC_LABEL } from '../../constants';
import { type PrometheusDatasource } from '../../datasource';
import { type PrometheusLanguageProviderInterface } from '../../language_provider';
import { getMockTimeRange } from '../../test/mocks/datasource';
import { MetricsBrowserProvider, useMetricsBrowser } from './MetricsBrowserContext';
const setupLocalStorageMock = () => {
let store: Record<string, string> = {};
return {
getItem: jest.fn((key: string) => store[key] || null),
setItem: jest.fn((key: string, value: string) => {
store[key] = value;
}),
clear: jest.fn(() => {
store = {};
}),
};
};
const localStorageMock = setupLocalStorageMock();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
/**
* Setup consistent mock response data for the language provider
*/
const setupLanguageProviderMock = () => {
const mockTimeRange = getMockTimeRange();
const mockLanguageProvider = {
retrieveMetrics: () => ['metric1', 'metric2', 'metric3'],
retrieveLabelKeys: () => ['__name__', 'instance', 'job', 'service'],
retrieveMetricsMetadata: () => ({
metric1: { type: 'counter', help: 'Test metric 1' },
metric2: { type: 'gauge', help: 'Test metric 2' },
}),
queryLabelKeys: jest.fn().mockResolvedValue(['__name__', 'instance', 'job', 'service']),
queryLabelValues: jest.fn().mockImplementation((_timeRange: TimeRange, label: string) => {
if (label === 'job') {
return Promise.resolve(['grafana', 'prometheus']);
}
if (label === 'instance') {
return Promise.resolve(['host1', 'host2']);
}
if (label === METRIC_LABEL) {
return Promise.resolve(['metric1', 'metric2', 'metric3']);
}
return Promise.resolve([]);
}),
} as unknown as PrometheusLanguageProviderInterface;
mockLanguageProvider.datasource = { seriesLimit: DEFAULT_SERIES_LIMIT } as unknown as PrometheusDatasource;
return { mockTimeRange, mockLanguageProvider };
};
/**
* Test component that renders context values and provides interaction buttons
*/
const TestComponent = () => {
const {
metrics,
labelKeys,
selectedMetric,
selectedLabelKeys,
onMetricClick,
onLabelKeyClick,
onLabelValueClick,
getSelector,
onClearClick,
validationStatus,
onValidationClick,
} = useMetricsBrowser();
return (
<div>
<div data-testid="metrics-count">{metrics.length}</div>
<div data-testid="labels-count">{labelKeys.length}</div>
<div data-testid="selected-metric">{selectedMetric}</div>
<div data-testid="selected-label-keys">{selectedLabelKeys.join(',')}</div>
<div data-testid="selector">{getSelector()}</div>
<div data-testid="validation-status">{validationStatus}</div>
<button data-testid="select-metric" onClick={() => onMetricClick('metric1')}>
Select Metric
</button>
<button data-testid="select-label" onClick={() => onLabelKeyClick('job')}>
Select Label
</button>
<button data-testid="select-label-value" onClick={() => onLabelValueClick('job', 'grafana', true)}>
Select Label Value
</button>
<button data-testid="clear" onClick={onClearClick}>
Clear
</button>
<button data-testid="validate" onClick={onValidationClick}>
Validate
</button>
</div>
);
};
/**
* Setup function for tests that returns mocks and render utilities
*/
const setupTest = () => {
const mockOnChange = jest.fn();
const { mockTimeRange, mockLanguageProvider } = setupLanguageProviderMock();
const renderWithProvider = (ui: ReactNode) => {
return render(
<MetricsBrowserProvider timeRange={mockTimeRange} languageProvider={mockLanguageProvider} onChange={mockOnChange}>
{ui}
</MetricsBrowserProvider>
);
};
return {
mockTimeRange,
mockLanguageProvider,
mockOnChange,
renderWithProvider,
};
};
describe('MetricsBrowserContext', () => {
beforeEach(() => {
jest.clearAllMocks();
localStorageMock.clear();
});
describe('basic functionality', () => {
it('should initialize and display metrics', async () => {
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Verify metrics are displayed in the UI
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
});
it('should restore selected labels from storage on initialization', async () => {
// Setup localStorage with saved preference
localStorageMock.setItem(LAST_USED_LABELS_KEY, JSON.stringify(['job', 'instance']));
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Verify the saved labels are loaded and displayed
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job,instance');
});
});
});
describe('user interactions', () => {
it('should select and deselect metrics', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Initially no metric is selected
expect(screen.getByTestId('selected-metric').textContent).toBe('');
// Select a metric
await user.click(screen.getByTestId('select-metric'));
// Verify selection in UI
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('metric1');
});
// Deselect by clicking the same metric
await user.click(screen.getByTestId('select-metric'));
// Verify the deselection in UI
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('');
});
});
it('should select and deselect label keys with persistence', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Initially no label key is selected
expect(screen.getByTestId('selected-label-keys').textContent).toBe('');
// Select a label key
await user.click(screen.getByTestId('select-label'));
// Verify UI update
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
});
// Deselect by clicking again
await user.click(screen.getByTestId('select-label'));
// Verify UI update
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('');
});
// Check localStorage was updated (not implementation but outcome)
const mockCalls = localStorageMock.setItem.mock.calls;
// Make sure we have calls to setItem
expect(mockCalls.length).toBeGreaterThan(0);
// Get the last call's arguments
const lastCall = mockCalls[mockCalls.length - 1];
expect(lastCall[0]).toBe(LAST_USED_LABELS_KEY);
expect(JSON.parse(lastCall[1])).toEqual([]);
});
it('should build a selector when selecting label values', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// First select a label key
await user.click(screen.getByTestId('select-label'));
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
});
// Select a label value
await user.click(screen.getByTestId('select-label-value'));
// Verify the selector is updated
await waitFor(() => {
expect(screen.getByTestId('selector').textContent).toBe('{job="grafana"}');
});
});
it('should use metric in selector when both metric and labels are selected', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Select a metric first
await user.click(screen.getByTestId('select-metric'));
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('metric1');
});
// Then select a label
await user.click(screen.getByTestId('select-label'));
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
});
// Then select a label value
await user.click(screen.getByTestId('select-label-value'));
// Verify the selector includes both metric and label
await waitFor(() => {
const selector = screen.getByTestId('selector').textContent;
expect(selector).toContain('metric1');
expect(selector).toContain('job="grafana"');
});
});
});
describe('selector operations', () => {
it('should clear all selections', async () => {
const user = userEvent.setup();
const { renderWithProvider, mockLanguageProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for initial data load
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Step 1: Select a metric
await user.click(screen.getByTestId('select-metric'));
await waitFor(() => {
expect(mockLanguageProvider.queryLabelKeys).toHaveBeenCalled();
expect(screen.getByTestId('selected-metric').textContent).toBe('metric1');
});
// Step 2: Select a label
await user.click(screen.getByTestId('select-label'));
await waitFor(() => {
expect(mockLanguageProvider.queryLabelValues).toHaveBeenCalledWith(
expect.anything(),
'job',
expect.anything(),
expect.anything()
);
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
});
// Step 3: Select a label value
await user.click(screen.getByTestId('select-label-value'));
await waitFor(() => {
expect(screen.getByTestId('selector').textContent).toContain('job="grafana"');
});
// Step 4: Clear all selections
await user.click(screen.getByTestId('clear'));
// Verify everything is cleared
await waitFor(() => {
// Check that all selections are cleared
expect(screen.getByTestId('selected-metric').textContent).toBe('');
expect(screen.getByTestId('selected-label-keys').textContent).toBe('');
expect(screen.getByTestId('selector').textContent).toBe('{}');
// Verify localStorage was cleared
const mockCalls = localStorageMock.setItem.mock.calls;
const lastCall = mockCalls[mockCalls.length - 1];
expect(lastCall[0]).toBe(LAST_USED_LABELS_KEY);
expect(JSON.parse(lastCall[1])).toEqual([]);
});
});
it('should validate selectors', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Create a valid selector
await user.click(screen.getByTestId('select-label'));
await user.click(screen.getByTestId('select-label-value'));
// Verify the selector is created
await waitFor(() => {
expect(screen.getByTestId('selector').textContent).toBe('{job="grafana"}');
});
// Trigger validation
await user.click(screen.getByTestId('validate'));
// Verify validation result
await waitFor(() => {
expect(screen.getByTestId('validation-status').textContent).toContain('Selector is valid');
});
});
});
describe('complete user workflows', () => {
it('should handle a full selection -> validation -> clear workflow', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// STEP 1: Select a metric
await user.click(screen.getByTestId('select-metric'));
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('metric1');
});
// STEP 2: Add a label
await user.click(screen.getByTestId('select-label'));
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
});
// STEP 3: Select a value
await user.click(screen.getByTestId('select-label-value'));
await waitFor(() => {
expect(screen.getByTestId('selector').textContent).toContain('job="grafana"');
});
// STEP 4: Validate
await user.click(screen.getByTestId('validate'));
await waitFor(() => {
expect(screen.getByTestId('validation-status').textContent).toContain('Selector is valid');
});
// STEP 5: Clear
await user.click(screen.getByTestId('clear'));
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('');
expect(screen.getByTestId('selected-label-keys').textContent).toBe('');
expect(screen.getByTestId('selector').textContent).toBe('{}');
});
});
});
describe('error handling', () => {
it('should throw error when hook is used outside provider', () => {
// Suppress console.error for this test
jest.spyOn(console, 'error').mockImplementation(() => {});
expect(() => {
renderHook(() => useMetricsBrowser());
}).toThrow('useMetricsBrowser must be used within a MetricsBrowserProvider');
// Restore console.error
(console.error as jest.Mock).mockRestore();
});
});
});

View File

@@ -1,167 +0,0 @@
import { createContext, type PropsWithChildren, useCallback, useContext, useMemo } from 'react';
import { type TimeRange } from '@grafana/data';
import { type PrometheusLanguageProviderInterface } from '../../language_provider';
import { buildSelector } from './selectorBuilder';
import { useMetricsLabelsValues } from './useMetricsLabelsValues';
export interface Metric {
name: string;
details?: string;
}
/**
* Context for the Metrics Browser component
* Provides state and handlers for browsing and selecting Prometheus metrics and labels
*/
interface MetricsBrowserContextType {
// Error and status state
err: string;
setErr: (err: string) => void;
status: string;
setStatus: (status: string) => void;
// Series limit settings
seriesLimit: number;
setSeriesLimit: (limit: number) => void;
// Callback when selector changes
onChange: (selector: string) => void;
// Data and selection state
metrics: Metric[];
labelKeys: string[];
isLoadingLabelKeys: boolean;
isLoadingLabelValues: boolean;
labelValues: Record<string, string[]>;
selectedMetric: string;
selectedLabelKeys: string[];
selectedLabelValues: Record<string, string[]>;
// Event handlers
onMetricClick: (name: string) => void;
onLabelKeyClick: (name: string) => void;
onLabelValueClick: (labelKey: string, labelValue: string, isSelected: boolean) => void;
getSelector: () => string;
onClearClick: () => void;
// Validation
validationStatus: string;
onValidationClick: () => void;
}
const MetricsBrowserContext = createContext<MetricsBrowserContextType | undefined>(undefined);
type MetricsBrowserProviderProps = {
timeRange: TimeRange;
languageProvider: PrometheusLanguageProviderInterface;
onChange: (selector: string) => void;
};
/**
* Provider component for the Metrics Browser context
* Manages state and data fetching for metrics, labels, and values
*/
export function MetricsBrowserProvider({
children,
timeRange,
languageProvider,
onChange,
}: PropsWithChildren<MetricsBrowserProviderProps>) {
const {
err,
setErr,
status,
setStatus,
seriesLimit,
setSeriesLimit,
validationStatus,
metrics,
labelKeys,
isLoadingLabelKeys,
isLoadingLabelValues,
labelValues,
selectedMetric,
selectedLabelKeys,
selectedLabelValues,
handleSelectedMetricChange,
handleSelectedLabelKeyChange,
handleSelectedLabelValueChange,
handleValidation,
handleClear,
} = useMetricsLabelsValues(timeRange, languageProvider);
// Build a Prometheus selector string from the current selections
const getSelector = useCallback(
() => buildSelector(selectedMetric, selectedLabelValues),
[selectedLabelValues, selectedMetric]
);
// Memoize the context value to prevent unnecessary re-renders
const value = useMemo(
() => ({
err,
setErr,
status,
setStatus,
seriesLimit,
setSeriesLimit,
validationStatus,
onChange,
getSelector,
metrics,
labelKeys,
isLoadingLabelKeys,
isLoadingLabelValues,
labelValues,
selectedMetric,
selectedLabelKeys,
selectedLabelValues,
onMetricClick: handleSelectedMetricChange,
onLabelKeyClick: handleSelectedLabelKeyChange,
onLabelValueClick: handleSelectedLabelValueChange,
onValidationClick: handleValidation,
onClearClick: handleClear,
}),
[
err,
setErr,
status,
setStatus,
seriesLimit,
setSeriesLimit,
validationStatus,
onChange,
getSelector,
metrics,
labelKeys,
isLoadingLabelKeys,
isLoadingLabelValues,
labelValues,
selectedMetric,
selectedLabelKeys,
selectedLabelValues,
handleSelectedMetricChange,
handleSelectedLabelKeyChange,
handleSelectedLabelValueChange,
handleValidation,
handleClear,
]
);
return <MetricsBrowserContext.Provider value={value}>{children}</MetricsBrowserContext.Provider>;
}
/**
* Hook to access the MetricsBrowser context
* Must be used within a MetricsBrowserProvider
*/
export function useMetricsBrowser() {
const context = useContext(MetricsBrowserContext);
if (context === undefined) {
throw new Error('useMetricsBrowser must be used within a MetricsBrowserProvider');
}
return context;
}

View File

@@ -1,95 +0,0 @@
import { cx } from '@emotion/css';
import { useMemo } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Button, Label, Stack, useStyles2 } from '@grafana/ui';
import { EMPTY_SELECTOR } from '../../constants';
import { useMetricsBrowser } from './MetricsBrowserContext';
import { getStylesSelectorActions } from './styles';
export function SelectorActions() {
const styles = useStyles2(getStylesSelectorActions);
const { validationStatus, onValidationClick, getSelector, onChange, status, err, onClearClick } = useMetricsBrowser();
const selector = getSelector();
const onClickRunQuery = () => {
onChange(selector);
};
const onClickRunRateQuery = () => {
const query = `rate(${selector}[$__rate_interval])`;
onChange(query);
};
const empty = useMemo(() => selector === EMPTY_SELECTOR, [selector]);
return (
<div className={styles.section}>
<Label>
<Trans i18nKey="grafana-prometheus.components.selector-actions.resulting-selector">4. Resulting selector</Trans>
</Label>
<div
aria-label={t('grafana-prometheus.components.selector-actions.aria-label-selector', 'selector')}
className={styles.selector}
>
{selector}
</div>
{validationStatus && <div className={styles.validationStatus}>{validationStatus}</div>}
<Stack>
<Button
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery}
aria-label={t(
'grafana-prometheus.components.selector-actions.aria-label-use-selector-for-query-button',
'Use selector for query button'
)}
disabled={empty}
onClick={onClickRunQuery}
>
<Trans i18nKey="grafana-prometheus.components.selector-actions.use-query">Use query</Trans>
</Button>
<Button
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useAsRateQuery}
aria-label={t(
'grafana-prometheus.components.selector-actions.aria-label-use-selector-as-metrics-button',
'Use selector as metrics button'
)}
variant="secondary"
disabled={empty}
onClick={onClickRunRateQuery}
>
<Trans i18nKey="grafana-prometheus.components.selector-actions.use-as-rate-query">Use as rate query</Trans>
</Button>
<Button
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.validateSelector}
aria-label={t(
'grafana-prometheus.components.selector-actions.aria-label-validate-submit-button',
'Validate submit button'
)}
variant="secondary"
disabled={empty}
onClick={onValidationClick}
>
<Trans i18nKey="grafana-prometheus.components.selector-actions.validate-selector">Validate selector</Trans>
</Button>
<Button
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.clear}
aria-label={t(
'grafana-prometheus.components.selector-actions.aria-label-selector-clear-button',
'Selector clear button'
)}
variant="secondary"
onClick={onClearClick}
>
<Trans i18nKey="grafana-prometheus.components.selector-actions.clear">Clear</Trans>
</Button>
<div className={cx(styles.status, (status || err) && styles.statusShowing)}>
<span className={err ? styles.error : ''}>{err || status}</span>
</div>
</Stack>
</div>
);
}

View File

@@ -1,112 +0,0 @@
import { useEffect, useState } from 'react';
import { FixedSizeList } from 'react-window';
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { BrowserLabel as PromLabel, Input, Label, useStyles2, Spinner } from '@grafana/ui';
import { LIST_ITEM_SIZE } from '../../constants';
import { useMetricsBrowser } from './MetricsBrowserContext';
import { getStylesMetricsBrowser, getStylesValueSelector } from './styles';
export function ValueSelector() {
const styles = useStyles2(getStylesValueSelector);
const sharedStyles = useStyles2(getStylesMetricsBrowser);
const [valueSearchTerm, setValueSearchTerm] = useState('');
const { labelValues, selectedLabelValues, isLoadingLabelValues, onLabelValueClick, onLabelKeyClick } =
useMetricsBrowser();
const [filteredLabelValues, setFilteredLabelValues] = useState<Record<string, string[]>>({ ...labelValues });
useEffect(() => {
const filtered: Record<string, string[]> = {};
for (const labelKey in labelValues) {
const values = labelValues[labelKey];
filtered[labelKey] = values.filter((value) => value.includes(valueSearchTerm));
}
setFilteredLabelValues(filtered);
}, [labelValues, valueSearchTerm]);
return (
<div className={styles.section}>
<Label
description={t(
'grafana-prometheus.components.value-selector.description-search-field-values-across-selected-labels',
'Use the search field to find values across selected labels.'
)}
>
<Trans i18nKey="grafana-prometheus.components.value-selector.select-multiple-values-for-your-labels">
3. Select (multiple) values for your labels
</Trans>
</Label>
<div>
<Input
onChange={(e) => setValueSearchTerm(e.currentTarget.value)}
aria-label={t(
'grafana-prometheus.components.value-selector.aria-label-filter-expression-for-label-values',
'Filter expression for label values'
)}
value={valueSearchTerm}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelValuesFilter}
/>
</div>
{isLoadingLabelValues ? (
<div className={sharedStyles.spinner}>
<Spinner size="xl" />
</div>
) : (
<div className={styles.valueListArea}>
{Object.entries(filteredLabelValues).map(([lk, lv]) => {
if (!lk || !lv) {
console.error('label values are empty:', { lk, lv });
return null;
}
return (
<div
role="list"
key={lk}
aria-label={t(
'grafana-prometheus.components.value-selector.aria-label-values-for',
'Values for {{labelKey}}',
{
labelKey: lk,
}
)}
className={styles.valueListWrapper}
>
<div className={styles.valueTitle}>
<PromLabel name={lk} active={true} hidden={false} facets={lv.length} onClick={onLabelKeyClick} />
</div>
<FixedSizeList
height={Math.min(200, LIST_ITEM_SIZE * (lv.length || 0))}
itemCount={lv.length || 0}
itemSize={28}
itemKey={(i) => lv[i]}
width={200}
className={styles.valueList}
>
{({ index, style }) => {
const value = lv[index];
const isSelected = selectedLabelValues[lk]?.includes(value);
return (
<div style={style}>
<PromLabel
name={value}
value={value}
active={isSelected}
onClick={(name) => onLabelValueClick(lk, name, !isSelected)}
searchTerm={valueSearchTerm}
/>
</div>
);
}}
</FixedSizeList>
</div>
);
})}
</div>
)}
</div>
);
}

View File

@@ -1,106 +0,0 @@
import { buildSelector } from './selectorBuilder';
describe('selectorBuilder', () => {
describe('buildSelector()', () => {
it('returns an empty selector for no labels', () => {
expect(buildSelector('', {})).toEqual('{}');
});
it('returns an empty selector for selected labels with no values', () => {
expect(buildSelector('', {})).toEqual('{}');
});
it('returns an empty selector for one selected label with no selected values', () => {
expect(buildSelector('', {})).toEqual('{}');
});
it('returns a simple selector from a selected label with a selected value', () => {
expect(buildSelector('', { foo: ['bar'] })).toEqual('{foo="bar"}');
});
it('metric selector without labels', () => {
expect(buildSelector('foo', {})).toEqual('foo{}');
});
it('metric selector with labels', () => {
expect(buildSelector('foo', { bar: ['baz'] })).toEqual('foo{bar="baz"}');
});
it('skips labels with empty value arrays', () => {
expect(buildSelector('metric', { emptyLabel: [], validLabel: ['value'] })).toEqual('metric{validLabel="value"}');
});
it('handles multiple values for a label using regex matcher', () => {
expect(buildSelector('', { multi: ['val1', 'val2', 'val3'] })).toEqual('{multi=~"(val1|val2|val3)"}');
});
it('properly escapes special characters in regex values', () => {
expect(buildSelector('', { special: ['val*', 'val.', 'val+'] })).toEqual(
'{special=~"(val\\\\*|val\\\\.|val\\\\+)"}'
);
});
it('properly escapes double quotes in exact matcher', () => {
expect(buildSelector('', { quoted: ['value"with"quotes'] })).toEqual('{quoted="value\\"with\\"quotes"}');
});
it('properly handles newlines in values', () => {
expect(buildSelector('', { newline: ['value\nwith\nnewlines'] })).toEqual('{newline="value\\nwith\\nnewlines"}');
});
it('combines multiple labels properly', () => {
expect(
buildSelector('', {
label1: ['value1'],
label2: ['value2'],
label3: ['value3'],
})
).toEqual('{label1="value1",label2="value2",label3="value3"}');
});
it('combines single and multi-value labels correctly', () => {
expect(
buildSelector('', {
single: ['value'],
multi: ['val1', 'val2'],
})
).toEqual('{single="value",multi=~"(val1|val2)"}');
});
describe('utf8 support', () => {
it('metric selector with utf8 metric', () => {
expect(buildSelector('utf8.metric', {})).toEqual('{"utf8.metric"}');
});
it('metric selector with utf8 labels', () => {
expect(buildSelector('foo', { 'utf8.label': ['baz'] })).toEqual('foo{"utf8.label"="baz"}');
});
it('metric selector with utf8 labels and metrics', () => {
expect(buildSelector('utf8.metric', { 'utf8.label': ['baz'] })).toEqual('{"utf8.metric","utf8.label"="baz"}');
});
it('metric selector with utf8 metric and with utf8/non-utf8 labels', () => {
expect(
buildSelector('utf8.metric', {
'utf8.label': ['uuu'],
bar: ['baz'],
})
).toEqual('{"utf8.metric","utf8.label"="uuu",bar="baz"}');
});
it('metric selector with non-utf8 metric with utf8/non-utf8 labels', () => {
expect(
buildSelector('foo', {
'utf8.label': ['uuu'],
bar: ['baz'],
})
).toEqual('foo{"utf8.label"="uuu",bar="baz"}');
});
it('handles utf8 characters in label values', () => {
expect(buildSelector('', { label: ['值', '😀', '你好'] })).toEqual('{label=~"(值|😀|你好)"}');
});
});
});
});

View File

@@ -1,48 +0,0 @@
import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../../escaping';
import { isValidLegacyName, utf8Support } from '../../utf8_support';
/**
* Builds a Prometheus selector string from a metric name and label values
* @param selectedMetric - The metric name, can be empty
* @param selectedLabelValues - Record of label names to their selected values
* @returns A properly formatted Prometheus selector string
*/
export function buildSelector(selectedMetric: string, selectedLabelValues: Record<string, string[]>): string {
// Handle empty case
if (selectedMetric === '' && Object.keys(selectedLabelValues).length === 0) {
return '{}';
}
// Build all label selectors
const selectorParts: string[] = [];
// Process label selectors
for (const [key, values] of Object.entries(selectedLabelValues)) {
// Skip empty value arrays
if (values.length === 0) {
continue;
}
// Use regex matcher for multiple values
if (values.length > 1) {
// Wrap alternation in parentheses for better regex performance: (val1|val2|val3) instead of val1|val2|val3
selectorParts.push(`${utf8Support(key)}=~"(${values.map(escapeLabelValueInRegexSelector).join('|')})"`);
} else {
// Use exact matcher for single value
selectorParts.push(`${utf8Support(key)}="${escapeLabelValueInExactSelector(values[0])}"`);
}
}
// Handle metric name cases
if (selectedMetric === '') {
return `{${selectorParts.join(',')}}`;
}
if (isValidLegacyName(selectedMetric)) {
return `${selectedMetric}{${selectorParts.join(',')}}`;
}
// Add quoted metric as another selector when it's not a valid legacy name
selectorParts.unshift(utf8Support(selectedMetric));
return `{${selectorParts.join(',')}}`;
}

View File

@@ -1,121 +0,0 @@
import { css } from '@emotion/css';
import { type GrafanaTheme2 } from '@grafana/data';
export const getStylesMetricsBrowser = (theme: GrafanaTheme2) => ({
wrapper: css({
backgroundColor: theme.colors.background.secondary,
padding: theme.spacing(1),
width: '100%',
borderRadius: theme.shape.radius.default,
}),
spinner: css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 120,
}),
});
export const getStylesMetricSelector = (theme: GrafanaTheme2) => ({
section: css({
'& + &': {
margin: `${theme.spacing(2)} 0`,
},
position: 'relative',
}),
valueListWrapper: css({
borderLeft: `1px solid ${theme.colors.border.medium}`,
margin: `${theme.spacing(1)} 0`,
padding: `${theme.spacing(1)} 0 ${theme.spacing(1)} ${theme.spacing(1)}`,
}),
valueList: css({
marginRight: theme.spacing(1),
resize: 'horizontal',
}),
});
export const getStylesLabelSelector = (theme: GrafanaTheme2) => ({
section: css({
'& + &': {
margin: `${theme.spacing(2)} 0`,
},
position: 'relative',
}),
list: css({
marginTop: theme.spacing(1),
display: 'flex',
flexWrap: 'wrap',
maxHeight: '200px',
overflow: 'auto',
alignContent: 'flex-start',
}),
});
export const getStylesValueSelector = (theme: GrafanaTheme2) => ({
section: css({
'& + &': {
margin: `${theme.spacing(2)} 0`,
},
position: 'relative',
}),
valueListArea: css({
display: 'flex',
flexWrap: 'wrap',
marginTop: theme.spacing(1),
}),
valueTitle: css({
marginLeft: `-${theme.spacing(0.5)}`,
marginBottom: theme.spacing(1),
}),
valueListWrapper: css({
borderLeft: `1px solid ${theme.colors.border.medium}`,
margin: `${theme.spacing(1)} 0`,
padding: `${theme.spacing(1)} 0 ${theme.spacing(1)} ${theme.spacing(1)}`,
}),
valueList: css({
marginRight: theme.spacing(1),
resize: 'horizontal',
}),
});
export const getStylesSelectorActions = (theme: GrafanaTheme2) => ({
section: css({
'& + &': {
margin: `${theme.spacing(2)} 0`,
},
position: 'relative',
}),
selector: css({
fontFamily: theme.typography.fontFamilyMonospace,
marginBottom: theme.spacing(1),
}),
status: css({
padding: theme.spacing(0.5),
color: theme.colors.text.secondary,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100%',
right: 0,
textAlign: 'right',
opacity: 0,
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transition: 'opacity 100ms linear',
},
}),
statusShowing: css({
opacity: 1,
}),
error: css({
color: theme.colors.error.main,
}),
validationStatus: css({
padding: theme.spacing(0.5),
marginBottom: theme.spacing(1),
color: theme.colors.text.maxContrast,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}),
});

View File

@@ -1,935 +0,0 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { type TimeRange } from '@grafana/data';
import { DEFAULT_SERIES_LIMIT, EMPTY_SELECTOR, LAST_USED_LABELS_KEY, METRIC_LABEL } from '../../constants';
import { type PrometheusDatasource } from '../../datasource';
import { PrometheusLanguageProvider, type PrometheusLanguageProviderInterface } from '../../language_provider';
import { getMockTimeRange } from '../../test/mocks/datasource';
import * as selectorBuilderModule from './selectorBuilder';
import { useMetricsLabelsValues } from './useMetricsLabelsValues';
// Test utilities to reduce boilerplate
const setupMocks = () => {
// Mock the buildSelector module
jest.spyOn(selectorBuilderModule, 'buildSelector').mockImplementation(() => EMPTY_SELECTOR);
// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem: jest.fn((key: string) => store[key] || null),
setItem: jest.fn((key: string, value: string) => {
store[key] = value;
}),
clear: jest.fn(() => {
store = {};
}),
};
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
// Mock language provider
const mockLanguageProvider: PrometheusLanguageProviderInterface = new PrometheusLanguageProvider({
seriesLimit: DEFAULT_SERIES_LIMIT,
} as unknown as PrometheusDatasource);
mockLanguageProvider.retrieveMetrics = jest.fn().mockReturnValue(['metric1', 'metric2', 'metric3']);
mockLanguageProvider.retrieveLabelKeys = jest.fn().mockReturnValue(['__name__', 'instance', 'job', 'service']);
mockLanguageProvider.retrieveMetricsMetadata = jest.fn().mockReturnValue({
metric1: { type: 'counter', help: 'Test metric 1' },
metric2: { type: 'gauge', help: 'Test metric 2' },
});
mockLanguageProvider.queryLabelValues = jest.fn();
mockLanguageProvider.queryLabelKeys = jest.fn();
// Mock standard responses
(mockLanguageProvider.queryLabelValues as jest.Mock).mockImplementation((_timeRange: TimeRange, label: string) => {
if (label === 'job') {
return Promise.resolve(['grafana', 'prometheus']);
}
if (label === 'instance') {
return Promise.resolve(['host1', 'host2']);
}
if (label === METRIC_LABEL) {
return Promise.resolve(['metric1', 'metric2', 'metric3']);
}
return Promise.resolve([]);
});
(mockLanguageProvider.queryLabelKeys as jest.Mock).mockImplementation((_timeRange: TimeRange, selector?: string) => {
if (selector) {
return Promise.resolve({
__name__: ['metric1', 'metric2'],
instance: ['instance1', 'instance2'],
job: ['job1', 'job2'],
service: ['service1', 'service2'],
});
}
return Promise.resolve(['__name__', 'instance', 'job', 'service']);
});
const mockTimeRange: TimeRange = getMockTimeRange();
return { mockLanguageProvider, mockTimeRange, localStorageMock };
};
// Helper to render hook with standard initialization
const renderHookWithInit = async (mocks: ReturnType<typeof setupMocks>) => {
const hookResult = renderHook(() => useMetricsLabelsValues(mocks.mockTimeRange, mocks.mockLanguageProvider));
// Wait for initialization
await act(async () => {
await waitFor(() => {
expect(mocks.mockLanguageProvider.queryLabelValues).toHaveBeenCalled();
});
// Wait for any additional state updates
await new Promise((resolve) => setTimeout(resolve, 0));
});
return hookResult;
};
describe('useMetricsLabelsValues', () => {
let mocks: ReturnType<typeof setupMocks>;
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
mocks = setupMocks();
jest.clearAllMocks();
// Spy on console.error to handle React warnings
consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(async () => {
// Cleanup any pending state updates
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
consoleSpy.mockRestore();
jest.restoreAllMocks();
});
// Helper function to wait for all state updates
const waitForStateUpdates = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
// Helper function to wait for debounce
const waitForDebounce = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 400));
});
};
describe('initialization', () => {
it('should initialize by fetching metrics', async () => {
renderHook(() => useMetricsLabelsValues(mocks.mockTimeRange, mocks.mockLanguageProvider));
await waitFor(() => {
expect(mocks.mockLanguageProvider.queryLabelValues).toHaveBeenCalled();
});
expect(mocks.mockLanguageProvider.queryLabelValues).toHaveBeenCalledWith(
expect.anything(),
METRIC_LABEL,
undefined,
DEFAULT_SERIES_LIMIT
);
});
it('should fetch label keys during initialization', async () => {
renderHook(() => useMetricsLabelsValues(mocks.mockTimeRange, mocks.mockLanguageProvider));
await waitFor(() => {
expect(mocks.mockLanguageProvider.queryLabelKeys).toHaveBeenCalled();
});
expect(mocks.mockLanguageProvider.queryLabelKeys).toHaveBeenCalledWith(
expect.anything(),
undefined,
DEFAULT_SERIES_LIMIT
);
});
it('should load saved label keys from localStorage and fetch values', async () => {
mocks.localStorageMock.setItem(LAST_USED_LABELS_KEY, JSON.stringify(['job', 'instance']));
renderHook(() => useMetricsLabelsValues(mocks.mockTimeRange, mocks.mockLanguageProvider));
await waitFor(() => {
const fetchCalls = (mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mock.calls;
const jobCall = fetchCalls.find((call) => call[1] === 'job');
const instanceCall = fetchCalls.find((call) => call[1] === 'instance');
return jobCall && instanceCall;
});
expect(mocks.mockLanguageProvider.queryLabelValues).toHaveBeenCalledWith(
expect.anything(),
'job',
undefined,
DEFAULT_SERIES_LIMIT
);
expect(mocks.mockLanguageProvider.queryLabelValues).toHaveBeenCalledWith(
expect.anything(),
'instance',
undefined,
DEFAULT_SERIES_LIMIT
);
});
});
describe('metric selection', () => {
it('should handle metric selection', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedMetricChange('metric1');
});
expect(result.current.selectedMetric).toBe('metric1');
expect(mocks.mockLanguageProvider.queryLabelKeys).toHaveBeenCalledWith(
expect.anything(),
undefined,
DEFAULT_SERIES_LIMIT
);
});
it('should clear metric selection when selecting same metric', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedMetricChange('metric1');
});
expect(result.current.selectedMetric).toBe('metric1');
await act(async () => {
await result.current.handleSelectedMetricChange('metric1');
});
expect(result.current.selectedMetric).toBe('');
});
it('should update label keys when metric is selected', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedMetricChange('metric1');
});
expect(mocks.mockLanguageProvider.queryLabelKeys).toHaveBeenCalled();
expect(result.current.labelKeys).toEqual(['__name__', 'instance', 'job', 'service']);
});
});
describe('label key selection', () => {
it('should handle label key selection', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
expect(result.current.selectedLabelKeys).toContain('job');
expect(mocks.mockLanguageProvider.queryLabelValues).toHaveBeenCalledWith(
expect.anything(),
'job',
undefined,
DEFAULT_SERIES_LIMIT
);
});
it('should handle label key deselection', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
expect(result.current.selectedLabelKeys).toContain('job');
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
expect(result.current.selectedLabelKeys).not.toContain('job');
expect(result.current.labelValues['job']).toBeUndefined();
});
it('should save selected label keys to localStorage', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
expect(mocks.localStorageMock.setItem).toHaveBeenCalledWith(LAST_USED_LABELS_KEY, JSON.stringify(['job']));
});
it('should fetch label values when label key is selected', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
expect(mocks.mockLanguageProvider.queryLabelValues).toHaveBeenCalledWith(
expect.anything(),
'job',
undefined,
DEFAULT_SERIES_LIMIT
);
expect(result.current.labelValues['job']).toEqual(['grafana', 'prometheus']);
});
});
describe('label value selection', () => {
it('should handle label value selection', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
await result.current.handleSelectedLabelValueChange('job', 'grafana', true);
});
expect(result.current.selectedLabelValues['job']).toContain('grafana');
expect(mocks.mockLanguageProvider.queryLabelValues).toHaveBeenCalled();
});
it('should handle label value deselection', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
await result.current.handleSelectedLabelValueChange('job', 'grafana', true);
});
expect(result.current.selectedLabelValues['job']).toContain('grafana');
await act(async () => {
await result.current.handleSelectedLabelValueChange('job', 'grafana', false);
});
expect(result.current.selectedLabelValues['job']).toBeUndefined();
});
it('should update metrics when label value is selected', async () => {
const { result } = await renderHookWithInit(mocks);
// Clear previous calls from initialization
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockClear();
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
await result.current.handleSelectedLabelValueChange('job', 'grafana', true);
});
// Get all calls to queryLabelValues after our actions
const calls = (mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mock.calls;
// Find the call that fetches metrics (__name__)
const metricsCall = calls.find((call) => call[1] === METRIC_LABEL);
expect(metricsCall).toBeTruthy();
expect(metricsCall![1]).toBe(METRIC_LABEL);
expect(metricsCall![3]).toBe(DEFAULT_SERIES_LIMIT);
});
it('should update other label values when a value is selected', async () => {
const { result } = await renderHookWithInit(mocks);
// Clear previous calls from initialization
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockClear();
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
await result.current.handleSelectedLabelKeyChange('instance');
await result.current.handleSelectedLabelValueChange('job', 'grafana', true);
});
// Get all calls to queryLabelValues after our actions
const calls = (mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mock.calls;
// Find the call that fetches metrics (__name__)
const metricsCall = calls.find((call) => call[1] === 'instance');
expect(metricsCall).toBeTruthy();
expect(metricsCall![1]).toBe('instance');
expect(metricsCall![3]).toBe(DEFAULT_SERIES_LIMIT);
});
});
describe('validation', () => {
it('should validate selector', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedMetricChange('metric1');
await result.current.handleSelectedLabelKeyChange('job');
await result.current.handleSelectedLabelValueChange('job', 'grafana', true);
await result.current.handleValidation();
});
expect(result.current.validationStatus).toContain('Selector is valid');
expect(mocks.mockLanguageProvider.queryLabelKeys).toHaveBeenCalled();
});
it('should handle validation errors', async () => {
const { result } = await renderHookWithInit(mocks);
(mocks.mockLanguageProvider.queryLabelKeys as jest.Mock).mockRejectedValueOnce(new Error('Test error'));
await act(async () => {
await result.current.handleValidation();
});
expect(result.current.err).toContain('Test error');
expect(result.current.validationStatus).toBe('');
});
});
describe('clear functionality', () => {
it('should clear all selections', async () => {
const { result } = await renderHookWithInit(mocks);
await act(async () => {
await result.current.handleSelectedMetricChange('metric1');
await result.current.handleSelectedLabelKeyChange('job');
await result.current.handleSelectedLabelValueChange('job', 'grafana', true);
});
await act(async () => {
await result.current.handleClear();
});
expect(result.current.selectedMetric).toBe('');
expect(result.current.selectedLabelKeys).toEqual([]);
expect(result.current.selectedLabelValues).toEqual({});
expect(result.current.err).toBe('');
expect(result.current.status).toBe('Ready');
expect(result.current.validationStatus).toBe('');
expect(mocks.localStorageMock.setItem).toHaveBeenCalledWith(LAST_USED_LABELS_KEY, '[]');
});
});
describe('error handling', () => {
it('should handle errors during metric fetch', async () => {
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockRejectedValueOnce(new Error('Metric fetch error'));
const { result } = await renderHookWithInit(mocks);
expect(result.current.err).toContain('Metric fetch error');
});
it('should handle errors during label value fetch', async () => {
const { result } = await renderHookWithInit(mocks);
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockRejectedValueOnce(
new Error('Label value fetch error')
);
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
expect(result.current.err).toContain('Label value fetch error');
});
it('should clear error state when new operation succeeds', async () => {
const { result } = await renderHookWithInit(mocks);
// Mock first call to fail
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock)
.mockRejectedValueOnce(new Error('Test error'))
// Mock subsequent calls to succeed
.mockResolvedValue(['value1', 'value2']);
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
expect(result.current.err).toContain('Test error');
await act(async () => {
await result.current.handleSelectedLabelKeyChange('instance');
});
// The error should be cleared by the successful operation
expect(result.current.err).toBe('');
});
});
describe('helper functions', () => {
describe('buildSafeSelector', () => {
it('should convert EMPTY_SELECTOR to undefined', async () => {
jest.spyOn(selectorBuilderModule, 'buildSelector').mockReturnValue(EMPTY_SELECTOR);
const { result } = await renderHookWithInit(mocks);
// Access the helper function
const buildSafeSelector = result.current.buildSafeSelector;
expect(buildSafeSelector('metric1', {})).toBeUndefined();
});
it('should return the selector value when not empty', async () => {
const expectedSelector = 'metric1{job="prometheus"}';
jest.spyOn(selectorBuilderModule, 'buildSelector').mockReturnValue(expectedSelector);
const { result } = await renderHookWithInit(mocks);
const buildSafeSelector = result.current.buildSafeSelector;
expect(buildSafeSelector('metric1', { job: ['prometheus'] })).toBe(expectedSelector);
});
});
describe('loadSelectedLabelsFromStorage', () => {
it('should filter labels against available labels', async () => {
mocks.localStorageMock.setItem(LAST_USED_LABELS_KEY, JSON.stringify(['job', 'instance', 'unavailable']));
const { result } = await renderHookWithInit(mocks);
const loadSelectedLabelsFromStorage = result.current.loadSelectedLabelsFromStorage;
const availableLabels = ['job', 'instance', 'pod'];
expect(loadSelectedLabelsFromStorage(availableLabels)).toEqual(['job', 'instance']);
});
it('should handle empty localStorage', async () => {
mocks.localStorageMock.clear();
const { result } = await renderHookWithInit(mocks);
const loadSelectedLabelsFromStorage = result.current.loadSelectedLabelsFromStorage;
expect(loadSelectedLabelsFromStorage(['job', 'instance'])).toEqual([]);
});
});
});
describe('seriesLimit handling', () => {
it('should refetch data when seriesLimit changes', async () => {
const { result, unmount } = renderHook(() =>
useMetricsLabelsValues(mocks.mockTimeRange, mocks.mockLanguageProvider)
);
// Wait for initial state updates
await waitForStateUpdates();
// Clear mock calls
jest.clearAllMocks();
// Change series limit
await act(async () => {
result.current.setSeriesLimit(1000 as unknown as typeof DEFAULT_SERIES_LIMIT);
await waitForDebounce();
});
// Verify data was refetched with new limit
await waitFor(() => {
const matchCalls = (mocks.mockLanguageProvider.queryLabelKeys as jest.Mock).mock.calls;
const callWithNewLimit = matchCalls.find((call) => call[2] === 1000);
expect(callWithNewLimit).toBeTruthy();
});
// Cleanup
unmount();
await waitForStateUpdates();
});
it('should use DEFAULT_SERIES_LIMIT when seriesLimit is empty', async () => {});
});
describe('timeRange handling', () => {
it('should not update timeRangeRef for small time changes', async () => {});
it('should update timeRangeRef for significant time changes', async () => {});
});
describe('testing with invalid values or special characters', () => {
it('should handle metric names with special characters', async () => {
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockImplementation(
(_timeRange: TimeRange, label: string) => {
if (label === METRIC_LABEL) {
return Promise.resolve(['metric-with-dash', 'metric.with.dots', 'metric{with}brackets']);
}
return Promise.resolve([]);
}
);
const { result } = await renderHookWithInit(mocks);
// Verify that metrics with special characters are returned
expect(result.current.metrics.map((m) => m.name)).toContain('metric-with-dash');
expect(result.current.metrics.map((m) => m.name)).toContain('metric.with.dots');
expect(result.current.metrics.map((m) => m.name)).toContain('metric{with}brackets');
// Try to select a metric with special characters
await act(async () => {
await result.current.handleSelectedMetricChange('metric{with}brackets');
});
// Verify the selection was successful
expect(result.current.selectedMetric).toBe('metric{with}brackets');
});
it('should handle label values with special characters', async () => {
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockImplementation(
(_timeRange: TimeRange, label: string) => {
if (label === 'job') {
return Promise.resolve(['name:with:colons', 'name/with/slashes', 'name=with=equals']);
}
if (label === METRIC_LABEL) {
return Promise.resolve(['metric1', 'metric2']);
}
return Promise.resolve([]);
}
);
// Set up selected label keys
mocks.localStorageMock.setItem(LAST_USED_LABELS_KEY, JSON.stringify(['job']));
const { result } = await renderHookWithInit(mocks);
// Verify special character label values are loaded
await waitFor(() => {
expect(result.current.labelValues.job).toContain('name:with:colons');
expect(result.current.labelValues.job).toContain('name/with/slashes');
expect(result.current.labelValues.job).toContain('name=with=equals');
});
// Test selecting a label value with special characters
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
await result.current.handleSelectedLabelValueChange('job', 'name:with:colons', true);
});
// Verify the selection was made
expect(result.current.selectedLabelValues.job).toContain('name:with:colons');
});
it('should handle empty strings in API responses', async () => {
// Mock API to return some empty strings
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockImplementation(
(_timeRange: TimeRange, label: string) => {
if (label === 'job') {
return Promise.resolve(['valid-job', '', 'another-job']);
}
if (label === METRIC_LABEL) {
return Promise.resolve(['metric1', 'metric2']);
}
return Promise.resolve([]);
}
);
// Set up selected label keys
mocks.localStorageMock.setItem(LAST_USED_LABELS_KEY, JSON.stringify(['job']));
const { result } = await renderHookWithInit(mocks);
// Verify empty string is included in values
await waitFor(() => {
expect(result.current.labelValues.job).toContain('');
});
// Test selecting an empty string as a value
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
await result.current.handleSelectedLabelValueChange('job', '', true);
});
// Verify the empty string was selected
expect(result.current.selectedLabelValues.job).toContain('');
});
});
describe('complete user workflows', () => {
it('should handle a full selection -> validation -> clear workflow', async () => {
const { result } = await renderHookWithInit(mocks);
// 1. Select a metric
await act(async () => {
await result.current.handleSelectedMetricChange('metric1');
});
expect(result.current.selectedMetric).toBe('metric1');
// 2. Add a label key
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
expect(result.current.selectedLabelKeys).toContain('job');
// 3. Select a label value
await act(async () => {
await result.current.handleSelectedLabelValueChange('job', 'grafana', true);
});
expect(result.current.selectedLabelValues.job).toContain('grafana');
// 4. Add another label key
await act(async () => {
await result.current.handleSelectedLabelKeyChange('instance');
});
expect(result.current.selectedLabelKeys).toContain('instance');
// 5. Select a value for the second label
await act(async () => {
await result.current.handleSelectedLabelValueChange('instance', 'host1', true);
});
expect(result.current.selectedLabelValues.instance).toContain('host1');
// 6. Validate the selection
// Mock the validation response
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockResolvedValue(['grafana', 'host1']);
await act(async () => {
await result.current.handleValidation();
});
expect(result.current.validationStatus).toContain('Selector is valid');
// 7. Clear everything
await act(async () => {
result.current.handleClear();
});
// 8. Verify everything was cleared
expect(result.current.selectedMetric).toBe('');
expect(result.current.selectedLabelKeys).toEqual([]);
expect(result.current.selectedLabelValues).toEqual({});
expect(result.current.validationStatus).toBe('');
});
it('should handle a workflow with deselections and reselections', async () => {
const { result } = await renderHookWithInit(mocks);
// 1. Select a metric
await act(async () => {
await result.current.handleSelectedMetricChange('metric1');
});
expect(result.current.selectedMetric).toBe('metric1');
// 2. Add label keys
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
// Wait for job to be in the selected keys
await waitFor(() => expect(result.current.selectedLabelKeys).toContain('job'));
// Wait for job values to be loaded
await waitFor(() => result.current.labelValues.job && result.current.labelValues.job.length > 0);
await act(async () => {
await result.current.handleSelectedLabelKeyChange('instance');
});
// Wait for instance to be in the selected keys
await waitFor(() => expect(result.current.selectedLabelKeys).toContain('instance'));
// Wait for instance values to be loaded
await waitFor(() => result.current.labelValues.instance && result.current.labelValues.instance.length > 0);
// 3. Select values
await act(async () => {
await result.current.handleSelectedLabelValueChange('job', 'grafana', true);
});
// Wait for job value to be selected
await waitFor(
() => result.current.selectedLabelValues.job && result.current.selectedLabelValues.job.includes('grafana')
);
await act(async () => {
await result.current.handleSelectedLabelValueChange('instance', 'host1', true);
});
// Wait for instance value to be selected
await waitFor(
() =>
result.current.selectedLabelValues.instance && result.current.selectedLabelValues.instance.includes('host1')
);
// 4. Deselect a value
await act(async () => result.current.handleSelectedLabelValueChange('job', 'grafana', false));
// Wait for job to be removed from selectedLabelValues
await waitFor(
() => !result.current.selectedLabelValues.job || result.current.selectedLabelValues.job.length === 0
);
expect(Object.keys(result.current.selectedLabelValues)).not.toContain('job');
// 5. Select a different value
await act(async () => {
await result.current.handleSelectedLabelValueChange('job', 'prometheus', true);
});
// Wait for new job value to be selected
await waitFor(
() => result.current.selectedLabelValues.job && result.current.selectedLabelValues.job.includes('prometheus')
);
expect(result.current.selectedLabelValues.job).toContain('prometheus');
// 6. Change metric selection
await act(async () => {
await result.current.handleSelectedMetricChange('metric2');
});
expect(result.current.selectedMetric).toBe('metric2');
// 7. Remove a label key
await act(async () => {
await result.current.handleSelectedLabelKeyChange('instance');
});
// Wait for instance to be removed from selectedLabelKeys
await waitFor(() => !result.current.selectedLabelKeys.includes('instance'));
expect(result.current.selectedLabelKeys).not.toContain('instance');
// 8. Validate
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockResolvedValue(['prometheus']);
await act(async () => {
await result.current.handleValidation();
});
expect(result.current.validationStatus).toContain('Selector is valid');
});
});
describe('state maintenance through complex interactions', () => {
it('should handle a complex workflow with changing label sets', async () => {
const { result } = await renderHookWithInit(mocks);
// 1. Select a metric
await act(async () => {
await result.current.handleSelectedMetricChange('metric1');
});
// 2. Add a label key and select a value
await act(async () => {
await result.current.handleSelectedLabelKeyChange('job');
});
// Wait for job to be in selectedLabelKeys
await waitFor(() => {
expect(result.current.selectedLabelKeys).toContain('job');
});
// Wait for job values to load
await waitFor(() => {
return result.current.labelValues.job && result.current.labelValues.job.length > 0;
});
// Select a job value
await act(async () => {
await result.current.handleSelectedLabelValueChange('job', 'grafana', true);
});
// Wait for job value to be selected
await waitFor(() => {
return result.current.selectedLabelValues.job && result.current.selectedLabelValues.job.includes('grafana');
});
// 3. Now mock that selecting a certain job value changes the available instance values
(mocks.mockLanguageProvider.queryLabelValues as jest.Mock).mockImplementation(
(_timeRange: TimeRange, label: string) => {
if (label === 'instance' && result.current.selectedLabelValues.job?.includes('grafana')) {
return Promise.resolve(['grafana-host1', 'grafana-host2']);
} else if (label === 'instance') {
return Promise.resolve(['host1', 'host2']);
}
if (label === 'job') {
return Promise.resolve(['grafana', 'prometheus']);
}
if (label === METRIC_LABEL) {
return Promise.resolve(['metric1', 'metric2', 'metric3']);
}
return Promise.resolve([]);
}
);
// 4. Add instance label
await act(async () => {
await result.current.handleSelectedLabelKeyChange('instance');
});
// Wait for instance to be in selectedLabelKeys
await waitFor(() => {
expect(result.current.selectedLabelKeys).toContain('instance');
});
// Wait for instance values to load
await waitFor(() => {
return result.current.labelValues.instance && result.current.labelValues.instance.length > 0;
});
// 5. Select a grafana-specific instance
await act(async () => {
await result.current.handleSelectedLabelValueChange('instance', 'grafana-host1', true);
});
// Wait for instance value to be selected
await waitFor(() => {
return (
result.current.selectedLabelValues.instance &&
result.current.selectedLabelValues.instance.includes('grafana-host1')
);
});
// Verify our current state for debugging
expect(result.current.selectedLabelValues.job).toBeDefined();
expect(result.current.selectedLabelValues.job).toContain('grafana');
expect(result.current.selectedLabelValues.instance).toBeDefined();
expect(result.current.selectedLabelValues.instance).toContain('grafana-host1');
// 6. First select a second job value to make sure the job array stays when we remove a value
await act(async () => {
await result.current.handleSelectedLabelValueChange('job', 'prometheus', true);
});
// Wait for both job values to be selected
await waitFor(() => {
return (
result.current.selectedLabelValues.job &&
result.current.selectedLabelValues.job.includes('grafana') &&
result.current.selectedLabelValues.job.includes('prometheus')
);
});
// Now deselect grafana
await act(async () => {
await result.current.handleSelectedLabelValueChange('job', 'grafana', false);
});
// Wait for grafana to be removed from job values but prometheus to remain
await waitFor(() => {
return (
result.current.selectedLabelValues.job &&
!result.current.selectedLabelValues.job.includes('grafana') &&
result.current.selectedLabelValues.job.includes('prometheus')
);
});
// Wait for instance values to update
await waitFor(() => {
return (
result.current.labelValues.instance &&
result.current.labelValues.instance.includes('host1') &&
!result.current.labelValues.instance.includes('grafana-host1')
);
});
// 7. Verify instance value was removed since it's no longer valid with the new job
expect(result.current.selectedLabelValues.instance || []).not.toContain('grafana-host1');
// 8. Verify the instance options have changed
expect(result.current.labelValues.instance).toContain('host1');
expect(result.current.labelValues.instance).toContain('host2');
expect(result.current.labelValues.instance).not.toContain('grafana-host1');
});
});
});

View File

@@ -1,421 +0,0 @@
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { useDebounce } from 'react-use';
import { type TimeRange } from '@grafana/data';
import { EMPTY_SELECTOR, LAST_USED_LABELS_KEY, METRIC_LABEL } from '../../constants';
import { type PrometheusLanguageProviderInterface } from '../../language_provider';
import { type Metric } from './MetricsBrowserContext';
import { buildSelector } from './selectorBuilder';
export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: PrometheusLanguageProviderInterface) => {
const timeRangeRef = useRef<TimeRange>(timeRange);
const lastSeriesLimitRef = useRef(languageProvider.datasource.seriesLimit);
const isInitializedRef = useRef(false);
const [seriesLimit, setSeriesLimit] = useState(languageProvider.datasource.seriesLimit);
const [err, setErr] = useState('');
const [status, setStatus] = useState('Ready');
const [validationStatus, setValidationStatus] = useState('');
const [metrics, setMetrics] = useState<Metric[]>([]);
const [selectedMetric, setSelectedMetric] = useState('');
const [labelKeys, setLabelKeys] = useState<string[]>([]);
const [selectedLabelKeys, setSelectedLabelKeys] = useState<string[]>([]);
const [lastSelectedLabelKey, setLastSelectedLabelKey] = useState('');
const [labelValues, setLabelValues] = useState<Record<string, string[]>>({});
const [selectedLabelValues, setSelectedLabelValues] = useState<Record<string, string[]>>({});
const [isLoadingLabelKeys, setIsLoadingLabelKeys] = useState(false);
const [isLoadingLabelValues, setIsLoadingLabelValues] = useState(false);
// Memoize the effective series limit to use the default when seriesLimit is empty
const effectiveLimit = useMemo(() => seriesLimit, [seriesLimit]);
// We don't want to trigger fetching for small amount of time changes.
// When MetricsBrowser re-renders for any reason we might receive a new timerange.
// This particularly happens when we have relative time ranges: from: now, to: now-1h
useEffect(() => {
if (
timeRange.to.diff(timeRangeRef.current.to, 'second') >= 5 &&
timeRange.from.diff(timeRangeRef.current.from, 'second') >= 5
) {
timeRangeRef.current = timeRange;
}
}, [timeRange]);
// Handler for error processing - logs the error and updates UI state
const handleError = useCallback((e: unknown, msg: string) => {
if (e instanceof Error) {
setErr(`${msg}: ${e.message}`);
} else {
setErr(`${msg}: Unknown error`);
}
setStatus('');
}, []);
// Get metadata details for a metric if available
const getMetricDetails = useCallback(
(metricName: string) => {
const meta = languageProvider.retrieveMetricsMetadata();
return meta && meta[metricName] ? `(${meta[metricName].type}) ${meta[metricName].help}` : undefined;
},
[languageProvider]
);
// Builds a safe selector string from metric name and label values
// Prometheus API doesn't allow empty matchers. This is bad => match[]={}
// Converts EMPTY_SELECTOR to undefined as some API calls need that
const buildSafeSelector = useCallback((metric: string, labelValues: Record<string, string[]>) => {
const selector = buildSelector(metric, labelValues);
return selector === EMPTY_SELECTOR ? undefined : selector;
}, []);
// Loads label keys from localStorage and filters them against available labels
// This ensures we only show label keys that are actually available in the current context
const loadSelectedLabelsFromStorage = useCallback(
(availableLabelKeys: string[]) => {
try {
const labelKeysInLocalStorageAsString = localStorage.getItem(LAST_USED_LABELS_KEY) || '[]';
const labelKeysInLocalStorage = JSON.parse(labelKeysInLocalStorageAsString);
return labelKeysInLocalStorage.filter((slk: string) => availableLabelKeys.includes(slk));
} catch (e) {
handleError(e, 'Failed to load saved label keys');
return [];
}
},
[handleError]
);
// Fetches metrics that match the given selector
// Transforms raw metric strings into Metric objects with metadata
const fetchMetrics = useCallback(
async (safeSelector?: string) => {
try {
const fetchedMetrics = await languageProvider.queryLabelValues(
timeRangeRef.current,
METRIC_LABEL,
safeSelector,
effectiveLimit
);
return fetchedMetrics.map((m) => ({
name: m,
details: getMetricDetails(m),
}));
} catch (e) {
handleError(e, 'Error fetching metrics');
return [];
}
},
[getMetricDetails, handleError, languageProvider, effectiveLimit]
);
// Fetches label keys based on an optional selector
// Uses different APIs depending on whether a selector is provided
const fetchLabelKeys = useCallback(
async (safeSelector?: string) => {
try {
return (
(await languageProvider.queryLabelKeys(timeRangeRef.current, safeSelector || undefined, effectiveLimit)) ?? []
);
} catch (e) {
handleError(e, 'Error fetching labels');
return [];
}
},
[handleError, languageProvider, effectiveLimit]
);
// Fetches values for multiple label keys and also prepares selected values
const fetchLabelValues = useCallback(
async (labelKeys: string[], safeSelector?: string) => {
const transformedLabelValues: Record<string, string[]> = {};
const newSelectedLabelValues: Record<string, string[]> = {};
for (const lk of labelKeys) {
try {
const values = await languageProvider.queryLabelValues(
timeRangeRef.current,
lk,
safeSelector,
effectiveLimit
);
transformedLabelValues[lk] = values;
if (selectedLabelValues[lk]) {
newSelectedLabelValues[lk] = [...selectedLabelValues[lk]];
}
setErr('');
} catch (e) {
handleError(e, 'Error fetching label values');
}
}
return [transformedLabelValues, newSelectedLabelValues];
},
[handleError, languageProvider, selectedLabelValues, effectiveLimit]
);
// Initial set up of the Metrics Browser
// This is called when "Clear" button clicked.
const initialize = useCallback(
async (metric: string, labelValues: Record<string, string[]>) => {
const selector = buildSelector(metric, labelValues);
const safeSelector = selector === EMPTY_SELECTOR ? undefined : selector;
// Metrics
const transformedMetrics: Metric[] = await fetchMetrics(safeSelector);
// Labels
setIsLoadingLabelKeys(true);
setIsLoadingLabelValues(true);
const transformedLabelKeys: string[] = await fetchLabelKeys(safeSelector);
// Selected Labels
const labelKeysInLocalStorage: string[] = loadSelectedLabelsFromStorage(transformedLabelKeys);
// Selected Labels' Values
const [transformedLabelValues] = await fetchLabelValues(labelKeysInLocalStorage, safeSelector);
setMetrics(transformedMetrics);
setLabelKeys(transformedLabelKeys);
setIsLoadingLabelKeys(false);
setSelectedLabelKeys(labelKeysInLocalStorage);
setLabelValues(transformedLabelValues);
setIsLoadingLabelValues(false);
},
[fetchLabelKeys, fetchLabelValues, fetchMetrics, loadSelectedLabelsFromStorage]
);
// Initialize the hook
useEffect(() => {
initialize(selectedMetric, selectedLabelValues);
isInitializedRef.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// We use debounce here to prevent fetching data on every keystroke
// We also track the seriesLimit change to prevent fetching twice right after the initialization
useDebounce(
() => {
if (isInitializedRef.current && lastSeriesLimitRef.current !== seriesLimit) {
initialize(selectedMetric, selectedLabelValues);
lastSeriesLimitRef.current = seriesLimit;
}
},
300,
[seriesLimit]
);
// Handles metric selection changes.
// If a metric selected it fetches the labels of that metric
// Otherwise it fetches all the labels.
// Based on the fetched labels, label value list is updated.
// If a label key is not present, its values are removed from the list.
const handleSelectedMetricChange = async (metricName: string) => {
const newSelectedMetric = selectedMetric !== metricName ? metricName : '';
const selector = buildSafeSelector(newSelectedMetric, selectedLabelValues);
try {
const fetchedMetrics = await fetchMetrics(selector);
setIsLoadingLabelKeys(true);
const fetchedLabelKeys = await fetchLabelKeys(selector);
const newSelectedLabelKeys = selectedLabelKeys.filter((slk) => fetchedLabelKeys.includes(slk));
setIsLoadingLabelValues(true);
const [transformedLabelValues, newSelectedLabelValues] = await fetchLabelValues(
newSelectedLabelKeys,
newSelectedMetric === '' ? undefined : selector
);
setMetrics(fetchedMetrics);
setSelectedMetric(newSelectedMetric);
setLabelKeys(fetchedLabelKeys);
setIsLoadingLabelKeys(false);
setSelectedLabelKeys(newSelectedLabelKeys);
setLabelValues(transformedLabelValues);
setIsLoadingLabelValues(false);
setSelectedLabelValues(newSelectedLabelValues);
} catch (e: unknown) {
handleError(e, 'Error fetching labels');
}
};
// Handles when a label key selection changed
// If it's a selection, it fetches the values based on the up-to-date selector
// If it's a de-selection, it clears the values from the list
const handleSelectedLabelKeyChange = async (labelKey: string) => {
const newSelectedLabelKeys = [...selectedLabelKeys];
const lkIdx = newSelectedLabelKeys.indexOf(labelKey);
const newLabelValues: Record<string, string[]> = { ...labelValues };
const newSelectedLabelValues: Record<string, string[]> = { ...selectedLabelValues };
if (lkIdx === -1) {
// Label key is not in the selectedLabelKeys. Let's add it.
newSelectedLabelKeys.push(labelKey);
const safeSelector = buildSafeSelector(selectedMetric, selectedLabelValues);
setIsLoadingLabelValues(true);
const [values] = await fetchLabelValues([labelKey], safeSelector);
newLabelValues[labelKey] = values[labelKey];
} else {
// Label key is in the selectedLabelKeys. Removing it and its values.
newSelectedLabelKeys.splice(lkIdx, 1);
delete newLabelValues[labelKey];
delete newSelectedLabelValues[labelKey];
}
localStorage.setItem(LAST_USED_LABELS_KEY, JSON.stringify(newSelectedLabelKeys));
setSelectedLabelKeys(newSelectedLabelKeys);
setLabelValues(newLabelValues);
setIsLoadingLabelValues(false);
setSelectedLabelValues(newSelectedLabelValues);
};
// Handle the labelValue click based on isSelected value.
// If it is false we need to remove it from selected values
// If it is true then we need to add it to selected values
// Then we first fetch the values of each selected label key using the up-to-date selector
// We merged the fetched and existing list for the list we interact.
// Because we might want to select more labels from the same list.
// For other value lists we use the intersection of fetched and selected values.
// Then we fetch the metrics based on new selector we have after value fetch
// Then we fetch the labels keys of the metrics we fetched.
const handleSelectedLabelValueChange = async (labelKey: string, labelValue: string, isSelected: boolean) => {
const newSelectedLabelValues = { ...selectedLabelValues };
let newLastSelectedLabelKey = lastSelectedLabelKey;
if (labelKey !== lastSelectedLabelKey) {
newLastSelectedLabelKey = labelKey;
}
// Label value selected
if (isSelected) {
if (!newSelectedLabelValues[labelKey]) {
newSelectedLabelValues[labelKey] = [];
}
newSelectedLabelValues[labelKey].push(labelValue);
} else {
newSelectedLabelValues[labelKey].splice(newSelectedLabelValues[labelKey].indexOf(labelValue), 1);
if (newSelectedLabelValues[labelKey].length === 0) {
delete newSelectedLabelValues[labelKey];
}
}
let safeSelector = buildSafeSelector(selectedMetric, newSelectedLabelValues);
// Fetch new values
let newLabelValues: Record<string, string[]> = {};
if (selectedLabelKeys.length !== 0) {
setIsLoadingLabelValues(true);
for (const lk of selectedLabelKeys) {
try {
const fetchedLabelValues = await languageProvider.queryLabelValues(
timeRange,
lk,
safeSelector,
effectiveLimit
);
// We don't want to discard values from last selected list.
// User might want to select more.
if (newLastSelectedLabelKey === lk) {
newLabelValues[lk] = Array.from(new Set([...labelValues[lk], ...fetchedLabelValues]));
} else {
// If there are already selected values merge them with the fetched values.
newLabelValues[lk] = fetchedLabelValues;
// Discard selected label values if they are not in response
newSelectedLabelValues[lk] = (newSelectedLabelValues[lk] ?? []).filter((item) =>
fetchedLabelValues.includes(item)
);
}
setErr('');
} catch (e: unknown) {
handleError(e, 'Error fetching label values');
}
}
}
// rebuild the selector based on the new selected label values
safeSelector = buildSafeSelector(selectedMetric, newSelectedLabelValues);
// Fetch metrics
const newMetrics: Metric[] = await fetchMetrics(safeSelector);
// Fetch label keys
// If there is no metric or label value selected fetch all the keys instead of creating a selector
setIsLoadingLabelKeys(true);
let newLabelKeys: string[] = [];
if (!safeSelector) {
newLabelKeys = await fetchLabelKeys(undefined);
} else {
const labelKeysSelector = `{${METRIC_LABEL}=~"${newMetrics.map((m) => m.name).join('|')}"}`;
newLabelKeys = await fetchLabelKeys(labelKeysSelector);
}
const newSelectedLabelKeys: string[] = loadSelectedLabelsFromStorage(newLabelKeys);
setMetrics(newMetrics);
setLabelKeys(newLabelKeys);
setIsLoadingLabelKeys(false);
setSelectedLabelKeys(newSelectedLabelKeys);
setLastSelectedLabelKey(newLastSelectedLabelKey);
setLabelValues(newLabelValues);
setIsLoadingLabelValues(false);
setSelectedLabelValues(newSelectedLabelValues);
};
// Validating if the selections we have can create a valid query
const handleValidation = async () => {
const selector = buildSelector(selectedMetric, selectedLabelValues);
setValidationStatus(`Validating selector ${selector}`);
setErr('');
try {
const results = await languageProvider.queryLabelKeys(timeRangeRef.current, selector, effectiveLimit);
setValidationStatus(`Selector is valid (${Object.keys(results).length} labels found)`);
} catch (e) {
handleError(e, 'Validation failed');
setValidationStatus('');
}
};
// Clears all the selections even the ones in localStorage
const handleClear = () => {
localStorage.setItem(LAST_USED_LABELS_KEY, '[]');
setSelectedMetric('');
setSelectedLabelKeys([]);
setSelectedLabelValues({});
setErr('');
setStatus('Ready');
setValidationStatus('');
initialize('', {});
};
return {
err,
setErr,
status,
setStatus,
seriesLimit,
setSeriesLimit,
validationStatus,
metrics,
labelKeys,
labelValues,
isLoadingLabelKeys,
isLoadingLabelValues,
selectedMetric,
selectedLabelKeys,
selectedLabelValues,
handleSelectedMetricChange,
handleSelectedLabelKeyChange,
handleSelectedLabelValueChange,
handleValidation,
handleClear,
// Helper functions - not part of the public API
buildSafeSelector,
loadSelectedLabelsFromStorage,
fetchMetrics,
fetchLabelKeys,
fetchLabelValues,
};
};

View File

@@ -1,328 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryField.tsx
import { css } from '@emotion/css';
import { parser } from '@prometheus-io/lezer-promql';
import { promLanguageDefinition } from 'monaco-promql';
import { useEffect, useRef } from 'react';
import { useLatest } from 'react-use';
import { v4 as uuidv4 } from 'uuid';
import { type GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { type Monaco, type monacoTypes, ReactMonacoEditor, useTheme2 } from '@grafana/ui';
import { type Props } from './MonacoQueryFieldProps';
import { getOverrideServices } from './getOverrideServices';
import { DataProvider } from './monaco-completion-provider/data_provider';
import { getCompletionProvider, getSuggestOptions } from './monaco-completion-provider/monaco-completion-provider';
import { placeHolderScopedVars, validateQuery } from './monaco-completion-provider/validation';
import { language, languageConfiguration } from './promql';
const options: monacoTypes.editor.IStandaloneEditorConstructionOptions = {
codeLens: false,
contextmenu: false,
// we need `fixedOverflowWidgets` because otherwise in grafana-dashboards
// the popup is clipped by the panel-visualizations.
fixedOverflowWidgets: true,
folding: false,
fontSize: 14,
lineDecorationsWidth: 8, // used as "padding-left"
lineNumbers: 'off',
minimap: { enabled: false },
overviewRulerBorder: false,
overviewRulerLanes: 0,
padding: {
// these numbers were picked so that visually this matches the previous version
// of the query-editor the best
top: 4,
bottom: 5,
},
renderLineHighlight: 'none',
scrollbar: {
vertical: 'hidden',
verticalScrollbarSize: 8, // used as "padding-right"
horizontal: 'hidden',
horizontalScrollbarSize: 0,
alwaysConsumeMouseWheel: false,
},
scrollBeyondLastLine: false,
suggest: getSuggestOptions(),
suggestFontSize: 12,
wordWrap: 'on',
quickSuggestionsDelay: 250,
};
// this number was chosen by testing various values. it might be necessary
// because of the width of the border, not sure.
//it needs to do 2 things:
// 1. when the editor is single-line, it should make the editor height be visually correct
// 2. when the editor is multi-line, the editor should not be "scrollable" (meaning,
// you do a scroll-movement in the editor, and it will scroll the content by a couple pixels
// up & down. this we want to avoid)
const EDITOR_HEIGHT_OFFSET = 2;
const PROMQL_LANG_ID = promLanguageDefinition.id;
// we must only run the promql-setup code once
let PROMQL_SETUP_STARTED = false;
function ensurePromQL(monaco: Monaco) {
if (PROMQL_SETUP_STARTED === false) {
PROMQL_SETUP_STARTED = true;
const { aliases, extensions, mimetypes } = promLanguageDefinition;
monaco.languages.register({ id: PROMQL_LANG_ID, aliases, extensions, mimetypes });
// @ts-ignore
monaco.languages.setMonarchTokensProvider(PROMQL_LANG_ID, language);
// @ts-ignore
monaco.languages.setLanguageConfiguration(PROMQL_LANG_ID, languageConfiguration);
}
}
const getStyles = (theme: GrafanaTheme2, placeholder: string) => {
return {
container: css({
borderRadius: theme.shape.radius.default,
border: `1px solid ${theme.components.input.borderColor}`,
display: 'flex',
flexDirection: 'row',
justifyContent: 'start',
alignItems: 'center',
height: '100%',
overflow: 'hidden',
}),
placeholder: css({
'::after': {
content: `'${placeholder}'`,
fontFamily: theme.typography.fontFamilyMonospace,
opacity: 0.6,
},
}),
};
};
const MonacoQueryField = (props: Props) => {
const id = uuidv4();
// we need only one instance of `overrideServices` during the lifetime of the react component
const overrideServicesRef = useRef(getOverrideServices());
const containerRef = useRef<HTMLDivElement>(null);
const { languageProvider, history, onBlur, onRunQuery, initialValue, placeholder, datasource, timeRange } = props;
const lpRef = useLatest(languageProvider);
const historyRef = useLatest(history);
const onRunQueryRef = useLatest(onRunQuery);
const onBlurRef = useLatest(onBlur);
const autocompleteDisposeFun = useRef<(() => void) | null>(null);
const theme = useTheme2();
const styles = getStyles(theme, placeholder);
useEffect(() => {
// when we unmount, we unregister the autocomplete-function, if it was registered
return () => {
autocompleteDisposeFun.current?.();
};
}, []);
return (
<div
data-testid={selectors.components.QueryField.container}
className={styles.container}
// NOTE: we will be setting inline-style-width/height on this element
ref={containerRef}
>
<ReactMonacoEditor
// see https://github.com/suren-atoyan/monaco-react/issues/365
saveViewState
overrideServices={overrideServicesRef.current}
options={options}
language="promql"
value={initialValue}
beforeMount={(monaco) => {
ensurePromQL(monaco);
}}
onMount={(editor, monaco) => {
const isEditorFocused = editor.createContextKey<boolean>('isEditorFocused' + id, false);
// we setup on-blur
editor.onDidBlurEditorWidget(() => {
isEditorFocused.set(false);
onBlurRef.current(editor.getValue());
});
editor.onDidFocusEditorText(() => {
isEditorFocused.set(true);
});
const dataProvider = new DataProvider({
historyProvider: historyRef.current,
languageProvider: lpRef.current,
});
// Create completion provider with state for Ctrl+Space detection
const { provider: completionProvider, state: completionState } = getCompletionProvider(
monaco,
dataProvider,
timeRange
);
// completion-providers in monaco are not registered directly to editor-instances,
// they are registered to languages. this makes it hard for us to have
// separate completion-providers for every query-field-instance
// (but we need that, because they might connect to different datasources).
// the trick we do is, we wrap the callback in a "proxy",
// and in the proxy, the first thing is, we check if we are called from
// "our editor instance", and if not, we just return nothing. if yes,
// we call the completion-provider.
const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = {
...completionProvider,
provideCompletionItems: (model, position, context, token) => {
// if the model-id does not match, then this call is from a different editor-instance,
// not "our instance", so return nothing
if (editor.getModel()?.id !== model.id) {
return { suggestions: [] };
}
return completionProvider.provideCompletionItems(model, position, context, token);
},
};
const { dispose } = monaco.languages.registerCompletionItemProvider(
PROMQL_LANG_ID,
filteringCompletionProvider
);
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.code === 'Space') {
// Only handle if this editor is focused
if (editor.hasTextFocus()) {
event.preventDefault();
event.stopPropagation();
completionState.isManualTriggerRequested = true;
editor.trigger('keyboard', 'editor.action.triggerSuggest', {});
setTimeout(() => {
completionState.isManualTriggerRequested = false;
}, 300);
}
}
};
// Add global listener
document.addEventListener('keydown', handleKeyDown, true);
// Combine cleanup functions
autocompleteDisposeFun.current = () => {
document.removeEventListener('keydown', handleKeyDown, true);
dispose();
};
// this code makes the editor resize itself so that the content fits
// (it will grow taller when necessary)
// FIXME: maybe move this functionality into CodeEditor, like:
// <CodeEditor resizingMode="single-line"/>
const updateElementHeight = () => {
const containerDiv = containerRef.current;
if (containerDiv !== null) {
const pixelHeight = editor.getContentHeight();
containerDiv.style.height = `${pixelHeight + EDITOR_HEIGHT_OFFSET}px`;
containerDiv.style.width = '100%';
const pixelWidth = containerDiv.clientWidth;
editor.layout({ width: pixelWidth, height: pixelHeight });
}
};
editor.onDidContentSizeChange(updateElementHeight);
updateElementHeight();
// handle: shift + enter
// FIXME: maybe move this functionality into CodeEditor?
editor.addCommand(
monaco.KeyMod.Shift | monaco.KeyCode.Enter,
() => {
onRunQueryRef.current(editor.getValue());
},
'isEditorFocused' + id
);
// Fixes Monaco capturing the search key binding and displaying a useless search box within the Editor.
// See https://github.com/grafana/grafana/issues/85850
monaco.editor.addKeybindingRule({
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyF,
command: null,
});
// Something in this configuration of monaco doesn't bubble up [mod]+K,
// which the command palette uses. Pass the event out of monaco manually
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, function () {
global.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true }));
});
if (placeholder) {
const placeholderDecorators = [
{
range: new monaco.Range(1, 1, 1, 1),
options: {
className: styles.placeholder,
isWholeLine: true,
},
},
];
let decorators: string[] = [];
const checkDecorators: () => void = () => {
const model = editor.getModel();
if (!model) {
return;
}
const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : [];
decorators = model.deltaDecorations(decorators, newDecorators);
};
checkDecorators();
editor.onDidChangeModelContent(checkDecorators);
editor.onDidChangeModelContent((e) => {
const model = editor.getModel();
if (!model) {
return;
}
const query = model.getValue();
const { errors, warnings } = validateQuery(
query,
datasource.interpolateString(query, placeHolderScopedVars),
model.getLinesContent(),
parser
);
const errorMarkers = errors.map(({ issue, ...boundary }) => {
return {
message: `${issue ? `Error parsing "${issue}"` : 'Parse error'}. The query appears to be incorrect and could fail to be executed.`,
severity: monaco.MarkerSeverity.Error,
...boundary,
};
});
const warningMarkers = warnings.map(({ issue, ...boundary }) => {
return {
message: `Warning: ${issue}`,
severity: monaco.MarkerSeverity.Warning,
...boundary,
};
});
monaco.editor.setModelMarkers(model, 'owner', [...errorMarkers, ...warningMarkers]);
});
}
}}
/>
</div>
);
};
// we will lazy-load this module using React.lazy,
// and that only supports default-exports,
// so we have to default-export this, even if
// it is against the style-guidelines.
export default MonacoQueryField;

View File

@@ -1,13 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryFieldLazy.tsx
import { Suspense } from 'react';
import MonacoQueryField from './MonacoQueryField';
import { type Props } from './MonacoQueryFieldProps';
export const MonacoQueryFieldLazy = (props: Props) => {
return (
<Suspense fallback={null}>
<MonacoQueryField {...props} />
</Suspense>
);
};

View File

@@ -1,21 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryFieldProps.ts
import { type HistoryItem, type TimeRange } from '@grafana/data';
import { type PrometheusDatasource } from '../../datasource';
import { type PrometheusLanguageProviderInterface } from '../../language_provider';
import { type PromQuery } from '../../types';
// we need to store this in a separate file,
// because we have an async-wrapper around,
// the react-component, and it needs the same
// props as the sync-component.
export type Props = {
initialValue: string;
languageProvider: PrometheusLanguageProviderInterface;
history: Array<HistoryItem<PromQuery>>;
placeholder: string;
onRunQuery: (value: string) => void;
onBlur: (value: string) => void;
datasource: PrometheusDatasource;
timeRange: TimeRange;
};

View File

@@ -1,27 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryFieldWrapper.tsx
import { useRef } from 'react';
import { MonacoQueryFieldLazy } from './MonacoQueryFieldLazy';
import { type Props as MonacoProps } from './MonacoQueryFieldProps';
type Props = Omit<MonacoProps, 'onRunQuery' | 'onBlur'> & {
onChange: (query: string) => void;
onRunQuery: () => void;
};
export const MonacoQueryFieldWrapper = (props: Props) => {
const lastRunValueRef = useRef<string | null>(null);
const { onRunQuery, onChange, ...rest } = props;
const handleRunQuery = (value: string) => {
lastRunValueRef.current = value;
onChange(value);
onRunQuery();
};
const handleBlur = (value: string) => {
onChange(value);
};
return <MonacoQueryFieldLazy onRunQuery={handleRunQuery} onBlur={handleBlur} {...rest} />;
};

View File

@@ -1,117 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/getOverrideServices.ts
import { type monacoTypes } from '@grafana/ui';
// this thing here is a workaround in a way.
// what we want to achieve, is that when the autocomplete-window
// opens, the "second, extra popup" with the extra help,
// also opens automatically.
// but there is no API to achieve it.
// the way to do it is to implement the `storageService`
// interface, and provide our custom implementation,
// which will default to `true` for the correct string-key.
// unfortunately, while the typescript-interface exists,
// it is not exported from monaco-editor,
// so we cannot rely on typescript to make sure
// we do it right. all we can do is to manually
// lookup the interface, and make sure we code our code right.
// our code is a "best effort" approach,
// i am not 100% how the `scope` and `target` things work,
// but so far it seems to work ok.
// i would use an another approach, if there was one available.
function makeStorageService() {
// we need to return an object that fulfills this interface:
// https://github.com/microsoft/vscode/blob/ff1e16eebb93af79fd6d7af1356c4003a120c563/src/vs/platform/storage/common/storage.ts#L37
// unfortunately it is not export from monaco-editor
const strings = new Map<string, string>();
// we want this to be true by default
strings.set('expandSuggestionDocs', true.toString());
return {
// we do not implement the on* handlers
onDidChangeValue: (data: unknown): void => undefined,
onDidChangeTarget: (data: unknown): void => undefined,
onWillSaveState: (data: unknown): void => undefined,
get: (key: string, scope: unknown, fallbackValue?: string): string | undefined => {
return strings.get(key) ?? fallbackValue;
},
getBoolean: (key: string, scope: unknown, fallbackValue?: boolean): boolean | undefined => {
const val = strings.get(key);
if (val !== undefined) {
// the interface-docs say the value will be converted
// to a boolean but do not specify how, so we improvise
return val === 'true';
} else {
return fallbackValue;
}
},
getNumber: (key: string, scope: unknown, fallbackValue?: number): number | undefined => {
const val = strings.get(key);
if (val !== undefined) {
return parseInt(val, 10);
} else {
return fallbackValue;
}
},
store: (
key: string,
value: string | boolean | number | undefined | null,
scope: unknown,
target: unknown
): void => {
// the interface-docs say if the value is nullish, it should act as delete
if (value === null || value === undefined) {
strings.delete(key);
} else {
strings.set(key, value.toString());
}
},
remove: (key: string, scope: unknown): void => {
strings.delete(key);
},
keys: (scope: unknown, target: unknown): string[] => {
return Array.from(strings.keys());
},
logStorage: (): void => {
console.log('logStorage: not implemented');
},
migrate: (): Promise<void> => {
// we do not implement this
return Promise.resolve(undefined);
},
isNew: (scope: unknown): boolean => {
// we create a new storage for every session, we do not persist it,
// so we return `true`.
return true;
},
flush: (reason?: unknown): Promise<void> => {
// we do not implement this
return Promise.resolve(undefined);
},
};
}
let overrideServices: monacoTypes.editor.IEditorOverrideServices | null = null;
export function getOverrideServices(): monacoTypes.editor.IEditorOverrideServices {
// only have one instance of this for every query editor
if (overrideServices === null) {
overrideServices = {
storageService: makeStorageService(),
};
}
return overrideServices;
}

View File

@@ -1,415 +0,0 @@
import { config } from '@grafana/runtime';
import { DEFAULT_COMPLETION_LIMIT } from '../../../constants';
import { getFunctions } from '../../../promql';
import { getMockTimeRange } from '../../../test/mocks/datasource';
import { filterMetricNames, getCompletions } from './completions';
import { DataProvider, type DataProviderParams } from './data_provider';
import type { Situation } from './situation';
const history: string[] = ['previous_metric_name_1', 'previous_metric_name_2', 'previous_metric_name_3'];
const dataProviderSettings = {
languageProvider: {
queryLabelKeys: jest.fn(),
queryLabelValues: jest.fn(),
queryMetricsMetadata: jest.fn().mockResolvedValue({}),
retrieveLabelKeys: jest.fn(),
retrieveMetricsMetadata: jest.fn().mockReturnValue({}),
},
historyProvider: history.map((expr, idx) => ({ query: { expr, refId: 'some-ref' }, ts: idx })),
} as unknown as DataProviderParams;
let dataProvider = new DataProvider(dataProviderSettings);
const metrics = {
beyondLimit: Array.from(Array(DEFAULT_COMPLETION_LIMIT + 1), (_, i) => `metric_name_${i}`),
get atLimit() {
return this.beyondLimit.slice(0, DEFAULT_COMPLETION_LIMIT - 1);
},
};
beforeEach(() => {
dataProvider = new DataProvider(dataProviderSettings);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('filterMetricNames', () => {
const sampleMetrics = [
'http_requests_total',
'http_requests_failed',
'node_cpu_seconds_total',
'node_memory_usage_bytes',
'very_long_metric_name_with_many_underscores_and_detailed_description',
'metric_name_1_with_extra_terms_included',
];
describe('empty input', () => {
it('should return all metrics up to limit when input is empty', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: '',
limit: 3,
});
expect(result).toEqual(sampleMetrics.slice(0, 3));
});
it('should return all metrics when input is whitespace', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: ' ',
limit: 3,
});
expect(result).toEqual(sampleMetrics.slice(0, 3));
});
});
describe('simple searches (≤ 4 terms)', () => {
it('should match exact strings', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_requests_total',
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
});
it('should match with single character errors', () => {
// substitution
let result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_requezts_total', // 's' replaced with 'z'
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
// ransposition
result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_reqeust_total', // 'ue' swapped
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
// deletion
result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_reqests_total', // missing 'u'
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
// insertion
result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'http_reqquests_total', // extra 'q'
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
});
it('should match partial strings', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'requests', // partial match
limit: 10,
});
expect(result).toContainEqual('http_requests_total');
expect(result).toContainEqual('http_requests_failed');
});
it('should not match with multiple errors', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'htp_reqests_total', // two errors: missing 't' and missing 'u'
limit: 10,
});
expect(result).not.toContainEqual('http_requests_total');
});
});
describe('complex searches (> 4 terms)', () => {
it('should use substring matching for each term', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'metric name 1 with extra terms',
limit: 10,
});
expect(result).toContainEqual('metric_name_1_with_extra_terms_included');
});
it('should return empty array when no metrics match all terms', () => {
const result = filterMetricNames({
metricNames: sampleMetrics,
inputText: 'metric name 1 with nonexistent terms',
limit: 10,
});
expect(result).toHaveLength(0);
});
it('should stop searching after limit is reached', () => {
const manyMetrics = Array.from({ length: 10 }, (_, i) => `metric_name_${i}_with_terms`);
const result = filterMetricNames({
metricNames: manyMetrics,
inputText: 'metric name with terms other words', // > 4 terms
limit: 3,
});
expect(result.length).toBeLessThanOrEqual(3);
});
});
});
type MetricNameSituation = Extract<Situation['type'], 'AT_ROOT' | 'EMPTY' | 'IN_FUNCTION'>;
const metricNameCompletionSituations = ['AT_ROOT', 'IN_FUNCTION', 'EMPTY'] as MetricNameSituation[];
function getSuggestionCountForSituation(situationType: MetricNameSituation, metricsCount: number): number {
const limitedMetricNamesCount = metricsCount < DEFAULT_COMPLETION_LIMIT ? metricsCount : DEFAULT_COMPLETION_LIMIT;
let suggestionsCount = limitedMetricNamesCount + getFunctions().length;
if (situationType === 'EMPTY') {
suggestionsCount += history.length;
}
return suggestionsCount;
}
describe.each(metricNameCompletionSituations)('metric name completions in situation %s', (situationType) => {
const timeRange = getMockTimeRange();
it('should return completions for all metric names when the number of metric names is at or below the limit', async () => {
jest.spyOn(dataProvider, 'queryMetricNames').mockResolvedValue(metrics.atLimit);
const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.atLimit.length);
const situation: Situation = {
type: situationType,
};
// No text input
dataProvider.monacoSettings.setInputInRange('');
let completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions).toHaveLength(expectedCompletionsCount);
// With text input (use fuzzy search)
dataProvider.monacoSettings.setInputInRange('name_1');
completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions?.length).toBeLessThanOrEqual(expectedCompletionsCount);
});
it('should limit completions for metric names when the number exceeds the limit', async () => {
const situation: Situation = {
type: situationType,
};
const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length);
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.beyondLimit);
// Complex query
dataProvider.monacoSettings.setInputInRange('metric name one two three four five');
let completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount);
// Simple query with fuzzy match
dataProvider.monacoSettings.setInputInRange('metric_name_');
completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount);
});
it('should handle complex queries efficiently', async () => {
const situation: Situation = {
type: situationType,
};
const testMetrics = ['metric_name_1', 'metric_name_2', 'metric_name_1_with_extra_terms', 'unrelated_metric'];
jest.spyOn(dataProvider, 'queryMetricNames').mockResolvedValue(testMetrics);
// Test with a complex query (> 4 terms)
dataProvider.monacoSettings.setInputInRange('metric name 1 with extra terms more');
const completions = await getCompletions(situation, dataProvider, timeRange);
const metricCompletions = completions.filter((c) => c.type === 'METRIC_NAME');
expect(metricCompletions.some((c) => c.label === 'metric_name_1_with_extra_terms')).toBe(true);
});
it('should handle multiple term queries efficiently', async () => {
const situation: Situation = {
type: situationType,
};
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.beyondLimit);
// Test with multiple terms
dataProvider.monacoSettings.setInputInRange('metric name 1 2 3 4 5');
const completions = await getCompletions(situation, dataProvider, timeRange);
const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length);
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount);
});
});
describe('Label value completions', () => {
let dataProvider: DataProvider;
beforeEach(() => {
dataProvider = {
getAllMetricNames: jest.fn(),
metricNamesToMetrics: jest.fn(),
getHistory: jest.fn(),
queryLabelValues: jest.fn().mockResolvedValue(['value1', 'value"2', 'value\\3', "value'4"]),
monacoSettings: {
setInputInRange: jest.fn(),
inputInRange: '',
suggestionsIncomplete: false,
enableAutocompleteSuggestionsUpdate: jest.fn(),
},
metricNamesSuggestionLimit: 100,
} as unknown as DataProvider;
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('with prometheusSpecialCharsInLabelValues disabled', () => {
beforeEach(() => {
jest.replaceProperty(config, 'featureToggles', {
prometheusSpecialCharsInLabelValues: false,
});
});
const timeRange = getMockTimeRange();
it('should not escape special characters when between quotes', async () => {
const situation: Situation = {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName: 'testLabel',
betweenQuotes: true,
otherLabels: [],
};
const completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions).toHaveLength(4);
expect(completions[0].insertText).toBe('value1');
expect(completions[1].insertText).toBe('value"2');
expect(completions[2].insertText).toBe('value\\3');
expect(completions[3].insertText).toBe("value'4");
});
it('should wrap in quotes but not escape special characters when not between quotes', async () => {
const situation: Situation = {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName: 'testLabel',
betweenQuotes: false,
otherLabels: [],
};
const completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions).toHaveLength(4);
expect(completions[0].insertText).toBe('"value1"');
expect(completions[1].insertText).toBe('"value"2"');
expect(completions[2].insertText).toBe('"value\\3"');
expect(completions[3].insertText).toBe('"value\'4"');
});
});
describe('with prometheusSpecialCharsInLabelValues enabled', () => {
beforeEach(() => {
jest.replaceProperty(config, 'featureToggles', {
prometheusSpecialCharsInLabelValues: true,
});
});
const timeRange = getMockTimeRange();
it('should escape special characters when between quotes', async () => {
const situation: Situation = {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName: 'testLabel',
betweenQuotes: true,
otherLabels: [],
};
const completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions).toHaveLength(4);
expect(completions[0].insertText).toBe('value1');
expect(completions[1].insertText).toBe('value\\"2');
expect(completions[2].insertText).toBe('value\\\\3');
expect(completions[3].insertText).toBe("value'4");
});
it('should wrap in quotes and escape special characters when not between quotes', async () => {
const situation: Situation = {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName: 'testLabel',
betweenQuotes: false,
otherLabels: [],
};
const completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions).toHaveLength(4);
expect(completions[0].insertText).toBe('"value1"');
expect(completions[1].insertText).toBe('"value\\"2"');
expect(completions[2].insertText).toBe('"value\\\\3"');
expect(completions[3].insertText).toBe('"value\'4"');
});
});
describe('label value escaping edge cases', () => {
beforeEach(() => {
jest.replaceProperty(config, 'featureToggles', {
prometheusSpecialCharsInLabelValues: true,
});
});
const timeRange = getMockTimeRange();
it('should handle empty values', async () => {
jest.spyOn(dataProvider, 'queryLabelValues').mockResolvedValue(['']);
const situation: Situation = {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName: 'testLabel',
betweenQuotes: false,
otherLabels: [],
};
const completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions).toHaveLength(1);
expect(completions[0].insertText).toBe('""');
});
it('should handle values with multiple special characters', async () => {
jest.spyOn(dataProvider, 'queryLabelValues').mockResolvedValue(['test"\\value']);
const situation: Situation = {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName: 'testLabel',
betweenQuotes: true,
otherLabels: [],
};
const completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions).toHaveLength(1);
expect(completions[0].insertText).toBe('test\\"\\\\value');
});
it('should handle non-string values', async () => {
jest.spyOn(dataProvider, 'queryLabelValues').mockResolvedValue([123 as unknown as string]);
const situation: Situation = {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName: 'testLabel',
betweenQuotes: false,
otherLabels: [],
};
const completions = await getCompletions(situation, dataProvider, timeRange);
expect(completions).toHaveLength(1);
expect(completions[0].insertText).toBe('"123"');
});
});
});

View File

@@ -1,296 +0,0 @@
// Core grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/completions.ts
import UFuzzy from '@leeoniya/ufuzzy';
import { type languages } from 'monaco-editor';
import { type TimeRange } from '@grafana/data';
import { config } from '@grafana/runtime';
import { DEFAULT_COMPLETION_LIMIT } from '../../../constants';
import { escapeLabelValueInExactSelector, prometheusRegularEscape } from '../../../escaping';
import { getFunctions } from '../../../promql';
import { isValidLegacyName } from '../../../utf8_support';
import { type DataProvider } from './data_provider';
import { type TriggerType } from './monaco-completion-provider';
import type { Label, Situation } from './situation';
import { NeverCaseError } from './util';
// FIXME: we should not load this from the "outside", but we cannot do that while we have the "old" query-field too
export type CompletionType = 'HISTORY' | 'FUNCTION' | 'METRIC_NAME' | 'DURATION' | 'LABEL_NAME' | 'LABEL_VALUE';
// We cannot use languages.CompletionItemInsertTextRule.InsertAsSnippet because grafana-prometheus package isn't compatible
// It should first change the moduleResolution to bundler for TS to correctly resolve the types
// https://github.com/grafana/grafana/pull/96450
const InsertAsSnippet = 4;
type Completion = {
type: CompletionType;
label: string;
insertText: string;
insertTextRules?: languages.CompletionItemInsertTextRule;
detail?: string;
documentation?: string;
triggerOnInsert?: boolean;
};
const metricNamesSearch = {
// see https://github.com/leeoniya/uFuzzy?tab=readme-ov-file#how-it-works for details
multiInsert: new UFuzzy({ intraMode: 0 }),
singleError: new UFuzzy({ intraMode: 1 }),
};
// Snippet Marker is telling monaco where to show the cursor and maybe a help text
// With help text example: ${1:labelName}
// labelName will be shown as selected. So user would know what to type next
const snippetMarker = '${1:}';
interface MetricFilterOptions {
metricNames: string[];
inputText: string;
limit: number;
}
export function filterMetricNames({ metricNames, inputText, limit }: MetricFilterOptions): string[] {
if (!inputText?.trim()) {
return metricNames.slice(0, limit);
}
const terms = metricNamesSearch.multiInsert.split(inputText); // e.g. 'some_metric_name or-another' -> ['some', 'metric', 'name', 'or', 'another']
const isComplexSearch = terms.length > 4;
const fuzzyResults = isComplexSearch
? metricNamesSearch.multiInsert.filter(metricNames, inputText) // for complex searches, prioritize performance by using MultiInsert fuzzy search
: metricNamesSearch.singleError.filter(metricNames, inputText); // for simple searches, prioritize flexibility by using SingleError fuzzy search
return fuzzyResults ? fuzzyResults.slice(0, limit).map((idx) => metricNames[idx]) : [];
}
// we order items like: history, functions, metrics
async function getAllMetricNamesCompletions(
searchTerm: string | undefined,
dataProvider: DataProvider,
timeRange: TimeRange
): Promise<Completion[]> {
let metricNames = await dataProvider.queryMetricNames(timeRange, searchTerm);
return dataProvider.metricNamesToMetrics(metricNames).map((metric) => ({
type: 'METRIC_NAME',
label: metric.name,
detail: `${metric.name} : ${metric.type}`,
documentation: metric.help,
...(metric.isUtf8
? {
insertText: `{"${metric.name}"${snippetMarker}}`,
insertTextRules: InsertAsSnippet,
}
: {
insertText: metric.name,
}),
}));
}
const getFunctionCompletions: () => Completion[] = () => {
return getFunctions().map((f) => ({
type: 'FUNCTION',
label: f.label,
insertText: f.insertText ?? '', // i don't know what to do when this is nullish. it should not be.
detail: f.detail,
documentation: f.documentation,
}));
};
async function getFunctionsOnlyCompletions(): Promise<Completion[]> {
return Promise.resolve(getFunctionCompletions());
}
async function getAllFunctionsAndMetricNamesCompletions(
searchTerm: string | undefined,
dataProvider: DataProvider,
timeRange: TimeRange
): Promise<Completion[]> {
const metricNames = await getAllMetricNamesCompletions(searchTerm, dataProvider, timeRange);
return [...getFunctionCompletions(), ...metricNames];
}
const DURATION_COMPLETIONS: Completion[] = [
'$__interval',
'$__range',
'$__rate_interval',
'1m',
'5m',
'10m',
'30m',
'1h',
'1d',
].map((text) => ({
type: 'DURATION',
label: text,
insertText: text,
}));
function getAllHistoryCompletions(dataProvider: DataProvider): Completion[] {
// function getAllHistoryCompletions(queryHistory: PromHistoryItem[]): Completion[] {
// NOTE: the typescript types are wrong. historyItem.query.expr can be undefined
const allHistory = dataProvider.getHistory();
// FIXME: find a better history-limit
return allHistory.slice(0, 10).map((expr) => ({
type: 'HISTORY',
label: expr,
insertText: expr,
}));
}
function makeSelector(metricName: string | undefined, labels: Label[]): string | undefined {
if (metricName === undefined && labels.length === 0) {
return undefined;
}
const allLabels = [...labels];
// we transform the metricName to a label, if it exists
if (metricName !== undefined) {
allLabels.push({ name: '__name__', value: metricName, op: '=' });
}
const allLabelTexts = allLabels.map(
(label) => `${label.name}${label.op}"${escapeLabelValueInExactSelector(label.value)}"`
);
return `{${allLabelTexts.join(',')}}`;
}
async function getLabelNames(
metric: string | undefined,
otherLabels: Label[],
dataProvider: DataProvider,
timeRange: TimeRange
): Promise<string[]> {
const selector = makeSelector(metric, otherLabels);
const labelNames = await dataProvider.queryLabelKeys(timeRange, selector, DEFAULT_COMPLETION_LIMIT);
// Exclude __name__ from output
otherLabels.push({ name: '__name__', value: '', op: '!=' });
const usedLabelNames = new Set(otherLabels.map((l) => l.name));
// names used in the query
return labelNames.filter((l) => !usedLabelNames.has(l));
}
async function getLabelNamesForCompletions(
metric: string | undefined,
suffix: string,
triggerOnInsert: boolean,
otherLabels: Label[],
dataProvider: DataProvider,
timeRange: TimeRange
): Promise<Completion[]> {
const labelNames = await getLabelNames(metric, otherLabels, dataProvider, timeRange);
return labelNames.map((text) => {
const isUtf8 = !isValidLegacyName(text);
return {
type: 'LABEL_NAME',
label: text,
...(isUtf8
? {
insertText: `"${text}"${suffix}`,
insertTextRules: InsertAsSnippet,
}
: {
insertText: `${text}${suffix}`,
}),
triggerOnInsert,
};
});
}
async function getLabelNamesForSelectorCompletions(
metric: string | undefined,
otherLabels: Label[],
dataProvider: DataProvider,
timeRange: TimeRange
): Promise<Completion[]> {
return getLabelNamesForCompletions(metric, '=', true, otherLabels, dataProvider, timeRange);
}
async function getLabelNamesForByCompletions(
metric: string | undefined,
otherLabels: Label[],
dataProvider: DataProvider,
timeRange: TimeRange
): Promise<Completion[]> {
return getLabelNamesForCompletions(metric, '', false, otherLabels, dataProvider, timeRange);
}
async function getLabelValues(
metric: string | undefined,
labelName: string,
otherLabels: Label[],
dataProvider: DataProvider,
timeRange: TimeRange
): Promise<string[]> {
const selector = makeSelector(metric, otherLabels);
return await dataProvider.queryLabelValues(timeRange, labelName, selector);
}
async function getLabelValuesForMetricCompletions(
metric: string | undefined,
labelName: string,
betweenQuotes: boolean,
otherLabels: Label[],
dataProvider: DataProvider,
timeRange: TimeRange
): Promise<Completion[]> {
const values = await getLabelValues(metric, labelName, otherLabels, dataProvider, timeRange);
return values.map((text) => ({
type: 'LABEL_VALUE',
label: text,
insertText: formatLabelValueForCompletion(text, betweenQuotes),
}));
}
function formatLabelValueForCompletion(value: string, betweenQuotes: boolean): string {
const text = config.featureToggles.prometheusSpecialCharsInLabelValues ? prometheusRegularEscape(value) : value;
return betweenQuotes ? text : `"${text}"`;
}
export async function getCompletions(
situation: Situation,
dataProvider: DataProvider,
timeRange: TimeRange,
searchTerm?: string,
triggerType: TriggerType = 'full'
): Promise<Completion[]> {
switch (situation.type) {
case 'IN_DURATION':
return Promise.resolve(DURATION_COMPLETIONS);
case 'IN_FUNCTION':
return triggerType === 'full'
? getAllFunctionsAndMetricNamesCompletions(searchTerm, dataProvider, timeRange)
: getFunctionsOnlyCompletions();
case 'AT_ROOT': {
return triggerType === 'full'
? getAllFunctionsAndMetricNamesCompletions(searchTerm, dataProvider, timeRange)
: getFunctionsOnlyCompletions();
}
case 'EMPTY': {
if (triggerType === 'partial') {
return Promise.resolve(getFunctionCompletions());
}
const metricNames = await getAllMetricNamesCompletions(searchTerm, dataProvider, timeRange);
const historyCompletions = getAllHistoryCompletions(dataProvider);
return Promise.resolve([...historyCompletions, ...getFunctionCompletions(), ...metricNames]);
}
case 'IN_LABEL_SELECTOR_NO_LABEL_NAME':
return getLabelNamesForSelectorCompletions(situation.metricName, situation.otherLabels, dataProvider, timeRange);
case 'IN_GROUPING':
return getLabelNamesForByCompletions(situation.metricName, situation.otherLabels, dataProvider, timeRange);
case 'IN_LABEL_SELECTOR_WITH_LABEL_NAME':
return getLabelValuesForMetricCompletions(
situation.metricName,
situation.labelName,
situation.betweenQuotes,
situation.otherLabels,
dataProvider,
timeRange
);
default:
throw new NeverCaseError(situation);
}
}

View File

@@ -1,33 +0,0 @@
import type { PrometheusLanguageProvider } from '../../../language_provider';
import { DataProvider, type DataProviderParams } from './data_provider';
const createLanguageProviderMock = (existingMetadata: Record<string, unknown> = {}) => ({
queryLabelKeys: jest.fn(),
queryLabelValues: jest.fn(),
queryMetricsMetadata: jest.fn().mockResolvedValue({}),
retrieveMetrics: jest.fn().mockReturnValue([]),
retrieveMetricsMetadata: jest.fn().mockReturnValue(existingMetadata),
});
const createDataProvider = (languageProvider: Partial<PrometheusLanguageProvider>) => {
return new DataProvider({ languageProvider } as DataProviderParams);
};
describe('DataProvider', () => {
describe('metadata fetching', () => {
it('calls queryMetricsMetadata when no metadata is cached', () => {
const languageProvider = createLanguageProviderMock({});
createDataProvider(languageProvider);
expect(languageProvider.queryMetricsMetadata).toHaveBeenCalledTimes(1);
});
it('does not call queryMetricsMetadata when metadata is already cached', () => {
const languageProvider = createLanguageProviderMock({
http_requests_total: { type: 'counter', help: 'Total HTTP requests' },
});
createDataProvider(languageProvider);
expect(languageProvider.queryMetricsMetadata).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,128 +0,0 @@
import { type HistoryItem, type TimeRange } from '@grafana/data';
import { DEFAULT_COMPLETION_LIMIT, METRIC_LABEL } from '../../../constants';
import { type PrometheusLanguageProviderInterface } from '../../../language_provider';
import { removeQuotesIfExist } from '../../../language_utils';
import { type PromQuery } from '../../../types';
import { escapeForUtf8Support, isValidLegacyName } from '../../../utf8_support';
export const CODE_MODE_SUGGESTIONS_INCOMPLETE_EVENT = 'codeModeSuggestionsIncomplete';
type SuggestionsIncompleteEvent = CustomEvent<{
limit: number;
datasourceUid: string;
}>;
export function isSuggestionsIncompleteEvent(e: Event): e is SuggestionsIncompleteEvent {
return (
e.type === CODE_MODE_SUGGESTIONS_INCOMPLETE_EVENT &&
'detail' in e &&
typeof e.detail === 'object' &&
e.detail !== null &&
'limit' in e.detail &&
'datasourceUid' in e.detail
);
}
interface Metric {
name: string;
help: string;
type: string;
isUtf8?: boolean;
}
export interface DataProviderParams {
languageProvider: PrometheusLanguageProviderInterface;
historyProvider: Array<HistoryItem<PromQuery>>;
}
export class DataProvider {
readonly languageProvider: PrometheusLanguageProviderInterface;
readonly historyProvider: Array<HistoryItem<PromQuery>>;
readonly queryLabelKeys: typeof this.languageProvider.queryLabelKeys;
readonly queryLabelValues: typeof this.languageProvider.queryLabelValues;
/**
* The text that's been typed so far within the current {@link Monaco.Range | Range}.
*
* @remarks
* This is useful with fuzzy searching items to provide as Monaco autocomplete suggestions.
*/
private inputInRange: string;
constructor(params: DataProviderParams) {
this.languageProvider = params.languageProvider;
this.historyProvider = params.historyProvider;
this.inputInRange = '';
this.queryLabelKeys = this.languageProvider.queryLabelKeys.bind(this.languageProvider);
this.queryLabelValues = this.languageProvider.queryLabelValues.bind(this.languageProvider);
// Ensure metadata is loaded for completions. The builder mode triggers this via its own
// components, but the code editor does not, so we need to fetch it here if not already cached.
const existingMetadata = this.languageProvider.retrieveMetricsMetadata();
if (Object.keys(existingMetadata).length === 0) {
this.languageProvider.queryMetricsMetadata();
}
}
/**
* Queries metric names with optional filtering.
* Safely constructs regex patterns and handles errors.
*/
queryMetricNames = async (timeRange: TimeRange, searchTerm: string | undefined): Promise<string[]> => {
try {
let match: string | undefined;
if (searchTerm) {
const escapedWord = escapeForUtf8Support(removeQuotesIfExist(searchTerm));
match = `{__name__=~".*${escapedWord}.*"}`;
}
const result = await this.languageProvider.queryLabelValues(
timeRange,
METRIC_LABEL,
match,
DEFAULT_COMPLETION_LIMIT
);
return Array.isArray(result) ? result : [];
} catch (error) {
console.warn('Failed to query metric names:', error);
return [];
}
};
getHistory(): string[] {
return this.historyProvider.map((h) => h.query.expr).filter(Boolean);
}
getAllMetricNames(): string[] {
return this.languageProvider.retrieveMetrics();
}
metricNamesToMetrics(metricNames: string[]): Metric[] {
const metricsMetadata = this.languageProvider.retrieveMetricsMetadata();
const result: Metric[] = metricNames.map((m) => {
const metaItem = metricsMetadata?.[m];
return {
name: m,
help: metaItem?.help ?? '',
type: metaItem?.type ?? '',
isUtf8: !isValidLegacyName(m),
};
});
return result;
}
private setInputInRange(textInput: string): void {
this.inputInRange = textInput;
}
get monacoSettings() {
return {
inputInRange: this.inputInRange,
setInputInRange: this.setInputInRange.bind(this),
};
}
}

View File

@@ -1,353 +0,0 @@
import { dateTime, type TimeRange } from '@grafana/data';
import type { Monaco, monacoTypes } from '@grafana/ui';
import { type DataProvider } from './data_provider';
import { getCompletionProvider, getSuggestOptions } from './monaco-completion-provider';
// Mock the dependencies
jest.mock('./completions');
jest.mock('./situation');
const mockGetCompletions = jest.fn();
const mockGetSituation = jest.fn();
jest.mock('./completions', () => ({
getCompletions: (...args: Parameters<typeof mockGetCompletions>) => mockGetCompletions(...args),
}));
jest.mock('./situation', () => ({
getSituation: (...args: Parameters<typeof mockGetSituation>) => mockGetSituation(...args),
}));
// Create proper Monaco mocks without 'any'
const createMockMonaco = (): Monaco => {
const mockRange = {
lift: jest.fn((range: monacoTypes.IRange) => range),
fromPositions: jest.fn((position: monacoTypes.Position) => ({
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: position.column,
endColumn: position.column,
})),
};
return {
languages: {
CompletionItemKind: {
Unit: 0,
Variable: 1,
Snippet: 2,
Enum: 3,
EnumMember: 4,
Constructor: 5,
} as const,
},
Range: mockRange,
} as unknown as Monaco;
};
const createMockModel = (
value: string,
mockWord: monacoTypes.editor.IWordAtPosition | null = null
): monacoTypes.editor.ITextModel => {
return {
getValue: () => value,
getValueInRange: jest.fn((range: monacoTypes.IRange) => {
// Convert to 0-based indexing
const startIndex = Math.max(0, range.startColumn - 1);
const endIndex = Math.min(value.length, range.endColumn - 1);
return value.substring(startIndex, endIndex);
}),
getWordAtPosition: jest.fn(() => mockWord),
getOffsetAt: jest.fn((position: monacoTypes.Position) => position.column - 1),
id: 'test-model',
} as unknown as monacoTypes.editor.ITextModel;
};
const createMockPosition = (column: number, lineNumber = 1): monacoTypes.Position =>
({
column,
lineNumber,
}) as monacoTypes.Position;
const createMockDataProvider = (): DataProvider => {
return {
monacoSettings: {
setInputInRange: jest.fn(),
suggestionsIncomplete: false,
},
} as unknown as DataProvider;
};
const createMockTimeRange = (): TimeRange => ({
from: dateTime(Date.now() - 3600000), // 1 hour ago
to: dateTime(Date.now()),
raw: { from: 'now-1h', to: 'now' },
});
describe('monaco-completion-provider', () => {
let monaco: Monaco;
let dataProvider: DataProvider;
let timeRange: TimeRange;
beforeEach(() => {
monaco = createMockMonaco();
dataProvider = createMockDataProvider();
timeRange = createMockTimeRange();
// Reset mocks
jest.clearAllMocks();
mockGetCompletions.mockResolvedValue([]);
mockGetSituation.mockReturnValue({ type: 'METRIC_NAME' });
// Mock window.getSelection
Object.defineProperty(window, 'getSelection', {
writable: true,
value: jest.fn(() => ({
toString: () => '',
})),
});
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('getSuggestOptions', () => {
it('should return options with showWords set to false', () => {
const options = getSuggestOptions();
expect(options).toEqual({
showWords: false,
});
});
});
describe('getCompletionProvider', () => {
it('should return provider and state objects', () => {
const result = getCompletionProvider(monaco, dataProvider, timeRange);
expect(result).toHaveProperty('provider');
expect(result).toHaveProperty('state');
expect(result.state).toHaveProperty('isManualTriggerRequested', false);
expect(result.provider).toHaveProperty('triggerCharacters');
expect(result.provider).toHaveProperty('provideCompletionItems');
});
it('should have correct trigger characters', () => {
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
expect(provider.triggerCharacters).toEqual(['{', ',', '[', '(', '=', '~', ' ', '"']);
});
});
describe('provideCompletionItems', () => {
it('should return empty suggestions when no situation is detected', async () => {
mockGetSituation.mockReturnValue(null);
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
const model = createMockModel('test');
const position = createMockPosition(4);
const result = await (provider.provideCompletionItems as Function)(model, position);
expect(result).toEqual({
suggestions: [],
incomplete: false,
});
});
it('should call getCompletions with correct parameters for normal word', async () => {
const mockWord = { word: 'grafana', startColumn: 1, endColumn: 7 };
const model = createMockModel('grafana', mockWord);
const position = createMockPosition(7);
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
await (provider.provideCompletionItems as Function)(model, position);
expect(mockGetCompletions).toHaveBeenCalledWith(
{ type: 'METRIC_NAME' },
dataProvider,
timeRange,
'grafana',
'full' // Should be 'full' because word length >= 3
);
});
it('should use partial trigger type for short words', async () => {
const mockWord = { word: 'go', startColumn: 1, endColumn: 3 };
const model = createMockModel('go', mockWord);
const position = createMockPosition(3);
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
await (provider.provideCompletionItems as Function)(model, position);
expect(mockGetCompletions).toHaveBeenCalledWith(
{ type: 'METRIC_NAME' },
dataProvider,
timeRange,
'go',
'partial' // Should be 'partial' because word length < 3
);
});
it('should format completion items correctly', async () => {
const mockCompletions = [
{
label: 'test_metric',
detail: 'A test metric',
insertText: 'test_metric',
documentation: 'Test documentation',
insertTextRules: undefined,
type: 'METRIC_NAME' as const,
triggerOnInsert: false,
},
];
mockGetCompletions.mockResolvedValue(mockCompletions);
const mockWord = { word: 'test', startColumn: 1, endColumn: 5 };
const model = createMockModel('test', mockWord);
const position = createMockPosition(5);
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
const result = await (provider.provideCompletionItems as Function)(model, position);
expect(result?.suggestions).toHaveLength(1);
expect(result?.suggestions?.[0]).toMatchObject({
label: 'test_metric',
detail: 'A test metric',
insertText: 'test_metric',
documentation: 'Test documentation',
kind: 5, // Constructor kind for METRIC_NAME
sortText: '0',
command: undefined,
});
});
it('should add trigger command for items with triggerOnInsert', async () => {
const mockCompletions = [
{
label: 'func(',
insertText: 'func(',
type: 'FUNCTION' as const,
triggerOnInsert: true,
},
];
mockGetCompletions.mockResolvedValue(mockCompletions);
const model = createMockModel('func');
const position = createMockPosition(4);
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
const result = await (provider.provideCompletionItems as Function)(model, position);
expect(result?.suggestions?.[0]?.command).toEqual({
id: 'editor.action.triggerSuggest',
title: '',
});
});
});
describe('manual trigger handling', () => {
it('should use full trigger type for manual trigger', async () => {
const mockWord = { word: 'te', startColumn: 1, endColumn: 3 };
const model = createMockModel('te', mockWord);
const position = createMockPosition(3);
const { provider, state } = getCompletionProvider(monaco, dataProvider, timeRange);
// Set manual trigger flag
state.isManualTriggerRequested = true;
await (provider.provideCompletionItems as Function)(model, position);
expect(mockGetCompletions).toHaveBeenCalledWith(
{ type: 'METRIC_NAME' },
dataProvider,
timeRange,
'te',
'full' // Should be 'full' despite short word length
);
});
});
describe('trigger character handling', () => {
const triggerCharacters = ['{', ',', '[', '(', '=', '~', ' ', '"'];
triggerCharacters.forEach((triggerChar) => {
it(`should use full trigger type for trigger character "${triggerChar}"`, async () => {
const testString = `grafana${triggerChar}`;
const model = createMockModel(testString, null);
const position = createMockPosition(testString.length + 1); // After trigger character (1-indexed)
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
await (provider.provideCompletionItems as Function)(model, position);
expect(mockGetCompletions).toHaveBeenCalledWith(
{ type: 'METRIC_NAME' },
dataProvider,
timeRange,
undefined, // No word at position after trigger char
'full'
);
});
});
it('should handle trigger character at beginning of line', async () => {
const model = createMockModel('{', null);
const position = createMockPosition(2); // After the { character
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
await (provider.provideCompletionItems as Function)(model, position);
// Should not fail and should still call getCompletions
expect(mockGetCompletions).toHaveBeenCalled();
});
});
describe('selection handling', () => {
it('should adjust cursor position when text is selected', async () => {
// Mock selected text
Object.defineProperty(window, 'getSelection', {
writable: true,
value: jest.fn(() => ({
toString: () => 'selected',
})),
});
const model = createMockModel('grafana selected');
const position = createMockPosition(16); // End of string
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
await (provider.provideCompletionItems as Function)(model, position);
// Should call getOffsetAt with adjusted position
expect(model.getOffsetAt).toHaveBeenCalledWith({
column: 8, // 16 - 8 (length of 'selected')
lineNumber: 1,
});
});
});
describe('data provider integration', () => {
it('should set input range on data provider', async () => {
const mockWord = { word: 'test', startColumn: 1, endColumn: 5 };
const model = createMockModel('test', mockWord);
const position = createMockPosition(5);
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
await (provider.provideCompletionItems as Function)(model, position);
expect(dataProvider.monacoSettings.setInputInRange).toHaveBeenCalled();
});
});
});

View File

@@ -1,179 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/index.ts
import { type TimeRange } from '@grafana/data';
import type { Monaco, monacoTypes } from '@grafana/ui';
import { type CompletionType, getCompletions } from './completions';
import { type DataProvider } from './data_provider';
import { getSituation } from './situation';
import { NeverCaseError } from './util';
export type TriggerType = 'partial' | 'full';
export type MonacoQueryFieldLocalState = {
isManualTriggerRequested: boolean;
};
const TRIGGER_CHARACTERS = ['{', ',', '[', '(', '=', '~', ' ', '"'];
const MIN_WORD_LENGTH_FOR_FULL_COMPLETIONS = 3;
export function getSuggestOptions(): monacoTypes.editor.ISuggestOptions {
return {
// monaco-editor sometimes provides suggestions automatically, i am not
// sure based on what, seems to be by analyzing the words already
// written.
// to try it out:
// - enter `go_goroutines{job~`
// - have the cursor at the end of the string
// - press ctrl-enter
// - you will get two suggestions
// those were not provided by grafana, they are offered automatically.
// i want to remove those. the only way i found is:
// - every suggestion-item has a `kind` attribute,
// that controls the icon to the left of the suggestion.
// - items auto-generated by monaco have `kind` set to `text`.
// - we make sure grafana-provided suggestions do not have `kind` set to `text`.
// - and then we tell monaco not to show suggestions of kind `text`
showWords: false,
};
}
function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind {
switch (type) {
case 'DURATION':
return monaco.languages.CompletionItemKind.Unit;
case 'FUNCTION':
return monaco.languages.CompletionItemKind.Variable;
case 'HISTORY':
return monaco.languages.CompletionItemKind.Snippet;
case 'LABEL_NAME':
return monaco.languages.CompletionItemKind.Enum;
case 'LABEL_VALUE':
return monaco.languages.CompletionItemKind.EnumMember;
case 'METRIC_NAME':
return monaco.languages.CompletionItemKind.Constructor;
default:
throw new NeverCaseError(type);
}
}
function getTriggerType(
word: monacoTypes.editor.IWordAtPosition | null,
model: monacoTypes.editor.ITextModel,
position: monacoTypes.Position,
state: MonacoQueryFieldLocalState
): TriggerType {
// Manual trigger (Ctrl+Space) - always full completions
if (state.isManualTriggerRequested) {
return 'full';
}
const charBeforeCursor = model.getValueInRange({
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: Math.max(1, position.column - 1),
endColumn: position.column,
});
if (TRIGGER_CHARACTERS.includes(charBeforeCursor)) {
return 'full';
}
// For typed words of sufficient length, use full completions
if (word && word.word.length >= MIN_WORD_LENGTH_FOR_FULL_COMPLETIONS) {
return 'full';
}
return 'partial';
}
export function getCompletionProvider(
monaco: Monaco,
dataProvider: DataProvider,
timeRange: TimeRange
): { provider: monacoTypes.languages.CompletionItemProvider; state: MonacoQueryFieldLocalState } {
const state: MonacoQueryFieldLocalState = {
isManualTriggerRequested: false,
};
const provideCompletionItems = (
model: monacoTypes.editor.ITextModel,
position: monacoTypes.Position
): monacoTypes.languages.ProviderResult<monacoTypes.languages.CompletionList> => {
const word = model.getWordAtPosition(position);
const range =
word != null
? monaco.Range.lift({
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
})
: monaco.Range.fromPositions(position);
// Set input range for data provider
dataProvider.monacoSettings.setInputInRange(model.getValueInRange(range));
// Get adjusted position for cursor/selection handling
const adjustedPosition = getAdjustedPosition(position);
const offset = model.getOffsetAt(adjustedPosition);
const situation = getSituation(model.getValue(), offset);
// Early exit if no situation detected
if (situation === null) {
return Promise.resolve({ suggestions: [], incomplete: false });
}
const triggerType: TriggerType = getTriggerType(word, model, position, state);
return getCompletions(situation, dataProvider, timeRange, word?.word, triggerType).then((items) => {
// Monaco by-default alphabetically orders the items.
// We use a number-as-string sortkey to maintain our custom order
const maxIndexDigits = items.length > 0 ? items.length.toString().length : 1;
const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({
kind: getMonacoCompletionItemKind(item.type, monaco),
label: item.label,
insertText: item.insertText,
insertTextRules: item.insertTextRules,
detail: item.detail,
documentation: item.documentation,
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
range,
command: item.triggerOnInsert
? {
id: 'editor.action.triggerSuggest',
title: '',
}
: undefined,
}));
return { suggestions };
});
};
// Helper function to handle position adjustment for selection
function getAdjustedPosition(position: monacoTypes.Position): { column: number; lineNumber: number } {
let adjustedColumn = position.column;
// Check to see if the browser supports window.getSelection()
if (window.getSelection) {
const selectedText = window.getSelection()?.toString();
// If the user has selected text, adjust the cursor position to be at the start of the selection
if (selectedText && selectedText.length > 0) {
adjustedColumn = Math.max(1, adjustedColumn - selectedText.length);
}
}
return {
column: adjustedColumn,
lineNumber: position.lineNumber,
};
}
return {
provider: {
triggerCharacters: TRIGGER_CHARACTERS,
provideCompletionItems,
},
state,
};
}

View File

@@ -1,324 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/situation.test.ts
import { getSituation, type Situation } from './situation';
// we use the `^` character as the cursor-marker in the string.
function assertSituation(situation: string, expectedSituation: Situation | null) {
// first we find the cursor-position
const pos = situation.indexOf('^');
if (pos === -1) {
throw new Error('cursor missing');
}
// we remove the cursor-marker from the string
const text = situation.replace('^', '');
// sanity check, make sure no more cursor-markers remain
if (text.indexOf('^') !== -1) {
throw new Error('multiple cursors');
}
const result = getSituation(text, pos);
if (expectedSituation === null) {
expect(result).toStrictEqual(null);
} else {
expect(result).toMatchObject(expectedSituation);
}
}
describe('situation', () => {
it('handles things', () => {
assertSituation('^', {
type: 'EMPTY',
});
assertSituation('sum(one) / ^', {
type: 'AT_ROOT',
});
assertSituation('sum(^)', {
type: 'IN_FUNCTION',
});
assertSituation('sum(one) / sum(^)', {
type: 'IN_FUNCTION',
});
assertSituation('something{}[^]', {
type: 'IN_DURATION',
});
assertSituation('something{label~^}', null);
});
it('handles label names', () => {
assertSituation('something{^}', {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
metricName: 'something',
otherLabels: [],
betweenQuotes: false,
});
assertSituation('sum(something) by (^)', {
type: 'IN_GROUPING',
metricName: 'something',
otherLabels: [],
});
assertSituation('sum by (^) (something)', {
type: 'IN_GROUPING',
metricName: 'something',
otherLabels: [],
});
assertSituation('something{one="val1",two!="val2",three=~"val3",four!~"val4",^}', {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
metricName: 'something',
otherLabels: [
{ name: 'one', value: 'val1', op: '=' },
{ name: 'two', value: 'val2', op: '!=' },
{ name: 'three', value: 'val3', op: '=~' },
{ name: 'four', value: 'val4', op: '!~' },
],
betweenQuotes: false,
});
assertSituation('{^}', {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
otherLabels: [],
betweenQuotes: false,
});
assertSituation('{one="val1",^}', {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
otherLabels: [{ name: 'one', value: 'val1', op: '=' }],
betweenQuotes: false,
});
// single-quoted label-values with escape
assertSituation("{one='val\\'1',^}", {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
otherLabels: [{ name: 'one', value: "val'1", op: '=' }],
betweenQuotes: false,
});
// double-quoted label-values with escape
assertSituation('{one="val\\"1",^}', {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
otherLabels: [{ name: 'one', value: 'val"1', op: '=' }],
betweenQuotes: false,
});
// backticked label-values with escape (the escape should not be interpreted)
assertSituation('{one=`val\\"1`,^}', {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
otherLabels: [{ name: 'one', value: 'val\\"1', op: '=' }],
betweenQuotes: false,
});
});
describe('utf-8 metric name support', () => {
it('with utf8 metric name no label and no comma', () => {
assertSituation(`{"metric.name"^}`, null);
});
it('with utf8 metric name no label', () => {
assertSituation(`{"metric.name", ^}`, {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
metricName: 'metric.name',
otherLabels: [],
betweenQuotes: false,
});
});
it('with utf8 metric name requesting utf8 labels in quotes', () => {
assertSituation(`{"metric.name", "^"}`, {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
metricName: 'metric.name',
otherLabels: [],
betweenQuotes: true,
});
});
it('with utf8 metric name with a legacy label', () => {
assertSituation(`{"metric.name", label1="val", ^}`, {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
metricName: 'metric.name',
otherLabels: [{ name: 'label1', value: 'val', op: '=' }],
betweenQuotes: false,
});
});
it('with utf8 metric name with a legacy label and no value', () => {
assertSituation(`{"metric.name", label1="^"}`, {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'metric.name',
labelName: 'label1',
betweenQuotes: true,
otherLabels: [],
});
});
it('with utf8 metric name with a utf8 label and no value', () => {
assertSituation(`{"metric.name", "utf8.label"="^"}`, {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'metric.name',
labelName: '"utf8.label"',
betweenQuotes: true,
otherLabels: [],
});
});
it('with utf8 metric name with a legacy label and utf8 label', () => {
assertSituation(`{"metric.name", label1="val", "utf8.label"="^"}`, {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'metric.name',
labelName: `"utf8.label"`,
betweenQuotes: true,
otherLabels: [{ name: 'label1', value: 'val', op: '=' }],
});
});
it('with utf8 metric name with a utf8 label and legacy label', () => {
assertSituation(`{"metric.name", "utf8.label"="val", label1="^"}`, {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'metric.name',
labelName: `label1`,
betweenQuotes: true,
otherLabels: [{ name: '"utf8.label"', value: 'val', op: '=' }],
});
});
it('with utf8 metric name with grouping', () => {
assertSituation(`sum by (^)(rate({"metric.name", label1="val"}[1m]))`, {
type: 'IN_GROUPING',
metricName: 'metric.name',
otherLabels: [],
});
});
});
it('utf-8 label support', () => {
assertSituation(`metric{"label": "^"}`, null);
assertSituation(`metric{"label with space": "^"}`, null);
assertSituation(`metric{"label_🤖": "^"}`, null);
assertSituation(`metric{"Spaß": "^"}`, null);
assertSituation(`{"metric", "Spaß": "^"}`, null);
assertSituation('something{"job"=^}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: '"job"',
betweenQuotes: false,
otherLabels: [],
});
assertSituation('something{"job📈"=^}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: '"job📈"',
betweenQuotes: false,
otherLabels: [],
});
assertSituation('something{"job with space"=^,host="h1"}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: '"job with space"',
betweenQuotes: false,
otherLabels: [{ name: 'host', value: 'h1', op: '=' }],
});
});
it('handles label values', () => {
assertSituation('something{job=^}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: 'job',
betweenQuotes: false,
otherLabels: [],
});
assertSituation('something{job!=^}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: 'job',
betweenQuotes: false,
otherLabels: [],
});
assertSituation('something{job=~^}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: 'job',
betweenQuotes: false,
otherLabels: [],
});
assertSituation('something{job!~^}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: 'job',
betweenQuotes: false,
otherLabels: [],
});
assertSituation('something{job=^,host="h1"}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: 'job',
betweenQuotes: false,
otherLabels: [{ name: 'host', value: 'h1', op: '=' }],
});
assertSituation('something{job="j1",host="^"}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: 'host',
betweenQuotes: true,
otherLabels: [{ name: 'job', value: 'j1', op: '=' }],
});
assertSituation('something{job="j1"^}', null);
assertSituation('something{job="j1" ^ }', null);
assertSituation('something{job="j1" ^ , }', null);
assertSituation('{job=^,host="h1"}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName: 'job',
betweenQuotes: false,
otherLabels: [{ name: 'host', value: 'h1', op: '=' }],
});
assertSituation('something{one="val1",two!="val2",three=^,four=~"val4",five!~"val5"}', {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
metricName: 'something',
labelName: 'three',
betweenQuotes: false,
otherLabels: [
{ name: 'one', value: 'val1', op: '=' },
{ name: 'two', value: 'val2', op: '!=' },
{ name: 'four', value: 'val4', op: '=~' },
{ name: 'five', value: 'val5', op: '!~' },
],
});
});
it('identifies all labels from queries when cursor is in middle', () => {
// Note the extra whitespace, if the cursor is after whitespace, the situation will fail to resolve
assertSituation('{one="val1", ^,two!="val2",three=~"val3",four!~"val4"}', {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
otherLabels: [
{ name: 'one', value: 'val1', op: '=' },
{ name: 'two', value: 'val2', op: '!=' },
{ name: 'three', value: 'val3', op: '=~' },
{ name: 'four', value: 'val4', op: '!~' },
],
betweenQuotes: false,
});
});
});

View File

@@ -1,612 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/situation.ts
import type { SyntaxNode, Tree } from '@lezer/common';
import {
AggregateExpr,
AggregateModifier,
BinaryExpr,
EqlRegex,
EqlSingle,
FunctionCallBody,
GroupingLabels,
Identifier,
LabelMatchers,
LabelName,
MatchOp,
MatrixSelector,
Neq,
NeqRegex,
NumberDurationLiteralInDurationContext,
parser,
PromQL,
QuotedLabelMatcher,
QuotedLabelName,
StringLiteral,
UnquotedLabelMatcher,
VectorSelector,
} from '@prometheus-io/lezer-promql';
import { NeverCaseError } from './util';
type Direction = 'parent' | 'firstChild' | 'lastChild' | 'nextSibling';
type NodeTypeId =
| 0 // this is used as error-id
| typeof AggregateExpr
| typeof AggregateModifier
| typeof FunctionCallBody
| typeof GroupingLabels
| typeof Identifier
| typeof UnquotedLabelMatcher
| typeof QuotedLabelMatcher
| typeof LabelMatchers
| typeof LabelName
| typeof QuotedLabelName
| typeof PromQL
| typeof StringLiteral
| typeof VectorSelector
| typeof MatrixSelector
| typeof MatchOp
| typeof EqlSingle
| typeof Neq
| typeof EqlRegex
| typeof NeqRegex;
type Path = Array<[Direction, NodeTypeId]>;
function move(node: SyntaxNode, direction: Direction): SyntaxNode | null {
switch (direction) {
case 'parent':
return node.parent;
case 'firstChild':
return node.firstChild;
case 'lastChild':
return node.lastChild;
case 'nextSibling':
return node.nextSibling;
default:
throw new NeverCaseError(direction);
}
}
function walk(node: SyntaxNode, path: Path): SyntaxNode | null {
let current: SyntaxNode | null = node;
for (const [direction, expectedType] of path) {
current = move(current, direction);
if (current === null) {
// we could not move in the direction, we stop
return null;
}
if (current.type.id !== expectedType) {
// the reached node has wrong type, we stop
return null;
}
}
return current;
}
function getNodeText(node: SyntaxNode, text: string, utf8?: boolean): string {
const nodeFrom = utf8 ? node.from + 1 : node.from;
const nodeTo = utf8 ? node.to - 1 : node.to;
return text.slice(nodeFrom, nodeTo);
}
function parsePromQLStringLiteral(text: string): string {
// if it is a string-literal, it is inside quotes of some kind
const inside = text.slice(1, text.length - 1);
// FIXME: support https://prometheus.io/docs/prometheus/latest/querying/basics/#string-literals
// FIXME: maybe check other promql code, if all is supported or not
// for now we do only some very simple un-escaping
// we start with double-quotes
if (text.startsWith('"') && text.endsWith('"')) {
// NOTE: this is not 100% perfect, we only unescape the double-quote,
// there might be other characters too
return inside.replace(/\\"/, '"');
}
// then single-quote
if (text.startsWith("'") && text.endsWith("'")) {
// NOTE: this is not 100% perfect, we only unescape the single-quote,
// there might be other characters too
return inside.replace(/\\'/, "'");
}
// then backticks
if (text.startsWith('`') && text.endsWith('`')) {
return inside;
}
throw new Error('FIXME: invalid string literal');
}
type LabelOperator = '=' | '!=' | '=~' | '!~';
export type Label = {
name: string;
value: string;
op: LabelOperator;
};
export type Situation =
| {
type: 'IN_FUNCTION';
}
| {
type: 'AT_ROOT';
}
| {
type: 'EMPTY';
}
| {
type: 'IN_DURATION';
}
| {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME';
metricName?: string;
otherLabels: Label[];
// utf8 labels must be in quotes
betweenQuotes: boolean;
}
| {
type: 'IN_GROUPING';
metricName: string;
otherLabels: Label[];
}
| {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME';
metricName?: string;
labelName: string;
betweenQuotes: boolean;
otherLabels: Label[];
};
type Resolver = {
path: NodeTypeId[];
fun: (node: SyntaxNode, text: string, pos: number) => Situation | null;
};
function isPathMatch(resolverPath: NodeTypeId[], cursorPath: number[]): boolean {
return resolverPath.every((item, index) => item === cursorPath[index]);
}
const ERROR_NODE_NAME: NodeTypeId = 0; // this is used as error-id
const RESOLVERS: Resolver[] = [
{
path: [LabelMatchers, VectorSelector],
fun: resolveLabelKeysWithEquals,
},
{
path: [StringLiteral, QuotedLabelName, LabelMatchers, VectorSelector],
fun: resolveUtf8LabelKeysWithEquals,
},
{
path: [PromQL],
fun: resolveTopLevel,
},
{
// Partially written metric name
path: [Identifier, VectorSelector, PromQL],
fun: resolveTopLevel,
},
{
path: [FunctionCallBody],
fun: resolveInFunction,
},
{
path: [StringLiteral, UnquotedLabelMatcher],
fun: resolveLabelMatcher,
},
{
path: [StringLiteral, QuotedLabelMatcher],
fun: resolveQuotedLabelMatcher,
},
{
path: [ERROR_NODE_NAME, BinaryExpr, PromQL],
fun: resolveTopLevel,
},
{
path: [ERROR_NODE_NAME, UnquotedLabelMatcher],
fun: resolveLabelMatcher,
},
{
path: [ERROR_NODE_NAME, QuotedLabelMatcher],
fun: resolveQuotedLabelMatcher,
},
{
path: [ERROR_NODE_NAME, NumberDurationLiteralInDurationContext, MatrixSelector],
fun: resolveDurations,
},
{
path: [GroupingLabels],
fun: resolveLabelsForGrouping,
},
];
const LABEL_OP_MAP = new Map<number, LabelOperator>([
[EqlSingle, '='],
[EqlRegex, '=~'],
[Neq, '!='],
[NeqRegex, '!~'],
]);
function getLabelOp(opNode: SyntaxNode): LabelOperator | null {
const opChild = opNode.firstChild;
if (opChild === null) {
return null;
}
return LABEL_OP_MAP.get(opChild.type.id) ?? null;
}
function getLabel(labelMatcherNode: SyntaxNode, text: string): Label | null {
const allowedMatchers = new Set([UnquotedLabelMatcher, QuotedLabelMatcher]);
if (!allowedMatchers.has(labelMatcherNode.type.id)) {
return null;
}
const nameNode =
walk(labelMatcherNode, [['firstChild', LabelName]]) ?? walk(labelMatcherNode, [['firstChild', QuotedLabelName]]);
if (nameNode === null) {
return null;
}
const opNode = walk(nameNode, [['nextSibling', MatchOp]]);
if (opNode === null) {
return null;
}
const op = getLabelOp(opNode);
if (op === null) {
return null;
}
const valueNode = walk(labelMatcherNode, [['lastChild', StringLiteral]]);
if (valueNode === null) {
return null;
}
const name = getNodeText(nameNode, text);
const value = parsePromQLStringLiteral(getNodeText(valueNode, text));
return { name, value, op };
}
function getLabels(labelMatchersNode: SyntaxNode, text: string): Label[] {
if (labelMatchersNode.type.id !== LabelMatchers) {
return [];
}
const matchers = [UnquotedLabelMatcher, QuotedLabelMatcher];
return matchers.reduce<Label[]>((acc, matcher) => {
labelMatchersNode.getChildren(matcher).forEach((ln) => {
const label = getLabel(ln, text);
if (notEmpty(label)) {
acc.push(label);
}
});
return acc;
}, []);
}
function getNodeChildren(node: SyntaxNode): SyntaxNode[] {
let child: SyntaxNode | null = node.firstChild;
const children: SyntaxNode[] = [];
while (child !== null) {
children.push(child);
child = child.nextSibling;
}
return children;
}
function getNodeInSubtree(node: SyntaxNode, typeId: NodeTypeId): SyntaxNode | null {
// first we try the current node
if (node.type.id === typeId) {
return node;
}
// then we try the children
const children = getNodeChildren(node);
for (const child of children) {
const n = getNodeInSubtree(child, typeId);
if (n !== null) {
return n;
}
}
return null;
}
function resolveLabelsForGrouping(node: SyntaxNode, text: string, pos: number): Situation | null {
const aggrExpNode = walk(node, [
['parent', AggregateModifier],
['parent', AggregateExpr],
]);
if (aggrExpNode === null) {
return null;
}
const bodyNode = aggrExpNode.getChild(FunctionCallBody);
if (bodyNode === null) {
return null;
}
const metricIdNode = getNodeInSubtree(bodyNode, Identifier) ?? getNodeInSubtree(bodyNode, StringLiteral);
if (!metricIdNode) {
return null;
}
// Let's check whether it's a utf8 metric.
// A utf8 metric must be a StringLiteral and its parent must be a QuotedLabelName
if (metricIdNode.type.id === StringLiteral && metricIdNode.parent?.type.id !== QuotedLabelName) {
return null;
}
const metricName = getNodeText(metricIdNode, text, metricIdNode.type.id === StringLiteral);
return {
type: 'IN_GROUPING',
metricName,
otherLabels: [],
};
}
function resolveLabelMatcher(node: SyntaxNode, text: string, pos: number): Situation | null {
// we can arrive here in two situation. `node` is either:
// - a StringNode (like in `{job="^"}`)
// - or an error node (like in `{job=^}`)
const inStringNode = !node.type.isError;
const parent = walk(node, [['parent', UnquotedLabelMatcher]]);
if (parent === null) {
return null;
}
const labelNameNode = walk(parent, [['firstChild', LabelName]]);
if (labelNameNode === null) {
return null;
}
const labelName = getNodeText(labelNameNode, text);
const labelMatchersNode = walk(parent, [['parent', LabelMatchers]]);
if (labelMatchersNode === null) {
return null;
}
// now we need to find the other names
const allLabels = getLabels(labelMatchersNode, text);
// we need to remove "our" label from all-labels, if it is in there
const otherLabels = allLabels.filter((label) => label.name !== labelName);
const metricName = getMetricName(labelMatchersNode, text);
// we are probably in a situation without a metric name
return {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName,
betweenQuotes: inStringNode,
otherLabels,
...(metricName ? { metricName } : {}),
};
}
function resolveQuotedLabelMatcher(node: SyntaxNode, text: string, pos: number): Situation | null {
// we can arrive here in two situation. `node` is either:
// - a StringNode (like in `{"job"="^"}`)
// - or an error node (like in `{"job"=^}`)
const inStringNode = !node.type.isError;
const parent = walk(node, [['parent', QuotedLabelMatcher]]);
if (parent === null) {
return null;
}
const labelNameNode = walk(parent, [['firstChild', QuotedLabelName]]);
if (labelNameNode === null) {
return null;
}
const labelName = getNodeText(labelNameNode, text);
const labelMatchersNode = walk(parent, [['parent', LabelMatchers]]);
if (labelMatchersNode === null) {
return null;
}
// now we need to find the other names
const allLabels = getLabels(labelMatchersNode, text);
// we need to remove "our" label from all-labels, if it is in there
const otherLabels = allLabels.filter((label) => label.name !== labelName);
const metricName = getMetricName(parent.parent!, text);
return {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
labelName,
betweenQuotes: inStringNode,
otherLabels,
...(metricName ? { metricName } : {}),
};
}
function resolveTopLevel(node: SyntaxNode, text: string, pos: number): Situation {
return {
type: 'AT_ROOT',
};
}
function resolveInFunction(node: SyntaxNode, text: string, pos: number): Situation {
return {
type: 'IN_FUNCTION',
};
}
function resolveDurations(node: SyntaxNode, text: string, pos: number): Situation {
return {
type: 'IN_DURATION',
};
}
function resolveLabelKeysWithEquals(node: SyntaxNode, text: string, pos: number): Situation | null {
// next false positive:
// `something{a="1"^}`
let child = walk(node, [['firstChild', UnquotedLabelMatcher]]);
if (child !== null) {
// means the label-matching part contains at least one label already.
//
// in this case, we will need to have a `,` character at the end,
// to be able to suggest adding the next label.
// the area between the end-of-the-child-node and the cursor-pos
// must contain a `,` in this case.
const textToCheck = text.slice(child.to, pos);
if (!textToCheck.includes(',')) {
return null;
}
}
// next false positive:
// `{"utf8.metric"^}`
child = walk(node, [['firstChild', QuotedLabelName]]);
if (child !== null) {
// means the label-matching part contains a utf8 metric.
//
// in this case, we will need to have a `,` character at the end,
// to be able to suggest adding the next label.
// the area between the end-of-the-child-node and the cursor-pos
// must contain a `,` in this case.
const textToCheck = text.slice(child.to, pos);
if (!textToCheck.includes(',')) {
return null;
}
}
const otherLabels = getLabels(node, text);
const metricName = getMetricName(node, text);
return {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
otherLabels,
betweenQuotes: false,
...(metricName ? { metricName } : {}),
};
}
function resolveUtf8LabelKeysWithEquals(node: SyntaxNode, text: string, pos: number): Situation | null {
const otherLabels = getLabels(node, text);
const metricName = node.parent?.parent ? getMetricName(node.parent.parent, text) : null;
return {
type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
otherLabels,
betweenQuotes: true,
...(metricName ? { metricName } : {}),
};
}
function getMetricName(node: SyntaxNode, text: string): string | null {
// Legacy Metric metric_name{label="value"}
const legacyMetricNameNode = walk(node, [
['parent', VectorSelector],
['firstChild', Identifier],
]);
if (legacyMetricNameNode) {
return getNodeText(legacyMetricNameNode, text);
}
// check for a utf-8 metric
// utf-8 metric {"metric.name", label="value"}
const utf8MetricNameNode = walk(node, [
['parent', VectorSelector],
['firstChild', LabelMatchers],
['firstChild', QuotedLabelName],
['firstChild', StringLiteral],
]);
if (utf8MetricNameNode) {
return getNodeText(utf8MetricNameNode, text, true);
}
// no metric name
return null;
}
// we find the first error-node in the tree that is at the cursor-position.
// NOTE: this might be too slow, might need to optimize it
// (ideas: we do not need to go into every subtree, based on from/to)
// also, only go to places that are in the sub-tree of the node found
// by default by lezer. problem is, `next()` will go upward too,
// and we do not want to go higher than our node
function getErrorNode(tree: Tree, pos: number): SyntaxNode | null {
const cur = tree.cursorAt(pos);
while (true) {
if (cur.from === pos && cur.to === pos) {
const { node } = cur;
if (node.type.isError) {
return node;
}
}
if (!cur.next()) {
break;
}
}
return null;
}
export function getSituation(text: string, pos: number): Situation | null {
// there is a special-case when we are at the start of writing text,
// so we handle that case first
if (text === '') {
return {
type: 'EMPTY',
};
}
/**
PromQL
Expr
VectorSelector
LabelMatchers
*/
const tree = parser.parse(text);
// if the tree contains error, it is very probable that
// our node is one of those error-nodes.
// also, if there are errors, the node lezer finds us,
// might not be the best node.
// so first we check if there is an error-node at the cursor-position
const maybeErrorNode = getErrorNode(tree, pos);
const cur = maybeErrorNode != null ? maybeErrorNode.cursor() : tree.cursorAt(pos);
const currentNode = cur.node;
const ids = [cur.type.id];
while (cur.parent()) {
ids.push(cur.type.id);
}
for (let resolver of RESOLVERS) {
// i do not use a foreach because i want to stop as soon
// as i find something
if (isPathMatch(resolver.path, ids)) {
return resolver.fun(currentNode, text, pos);
}
}
return null;
}
function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}

View File

@@ -1,27 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/util.ts
// this helper class is used to make typescript warn you when you forget
// a case-block in a switch statement.
// example code that triggers the typescript-error:
//
// const x:'A'|'B'|'C' = 'A';
//
// switch(x) {
// case 'A':
// // something
// case 'B':
// // something
// default:
// throw new NeverCaseError(x);
// }
//
//
// typescript will show an error in this case,
// when you add the missing `case 'C'` code,
// the problem will be fixed.
export class NeverCaseError extends Error {
constructor(value: never) {
super('should never happen');
}
}

View File

@@ -1,89 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/validation.test.ts
import { parser } from '@prometheus-io/lezer-promql';
import { validateQuery, warningTypes } from './validation';
describe('Monaco Query Validation', () => {
test('Identifies empty queries as valid', () => {
expect(validateQuery('', '', [], parser)).toEqual({ errors: [], warnings: [] });
});
test.each([
'access_evaluation_duration_sum{job="grafana"}',
'http_requests_total{job="apiserver", handler="/api/comments"}[5m]',
'http_requests_total{job=~".*server"}',
'rate(http_requests_total[5m])[30m:1m]',
'max_over_time(deriv(rate(distance_covered_total[5s])[30s:5s])[10m:])',
'rate(http_requests_total[5m])',
'topk(3, sum by (app, proc) (rate(instance_cpu_time_ns[5m])))',
])('Identifies valid queries', (query: string) => {
expect(validateQuery(query, query, [], parser).errors).toEqual([]);
});
test('Identifies invalid queries', () => {
// Missing } at the end
let query = 'access_evaluation_duration_sum{job="grafana"';
expect(validateQuery(query, query, [query], parser)).toEqual({
errors: [{ endColumn: 45, endLineNumber: 1, issue: '{job="grafana"', startColumn: 31, startLineNumber: 1 }],
warnings: [],
});
// Missing handler="value"
query = 'http_requests_total{job="apiserver", handler}[5m]';
expect(validateQuery(query, query, [query], parser)).toEqual({
errors: [{ endColumn: 45, endLineNumber: 1, issue: 'handler', startColumn: 38, startLineNumber: 1 }],
warnings: [],
});
// Missing : in [30s:5s]
query = 'max_over_time(deriv(rate(distance_covered_total[5s])[30s5s])[10m:])';
expect(validateQuery(query, query, [query], parser)).toEqual({
errors: [{ endColumn: 59, endLineNumber: 1, issue: '5s', startColumn: 57, startLineNumber: 1 }],
warnings: [],
});
});
test('Identifies valid multi-line queries', () => {
const query = `
sum by (job) (
rate(http_requests_total[5m])
)`;
const queryLines = query.split('\n');
expect(validateQuery(query, query, queryLines, parser)).toEqual({ errors: [], warnings: [] });
});
test('Identifies invalid multi-line queries', () => {
const query = `
sum by (job) (
rate(http_requests_total[])
)`;
const queryLines = query.split('\n');
expect(validateQuery(query, query, queryLines, parser)).toEqual({
errors: [{ endColumn: 30, endLineNumber: 3, issue: '', startColumn: 30, startLineNumber: 3 }],
warnings: [],
});
});
test('Warns agains subqueries with same duration and step', () => {
const query = 'rate(http_requests_total[5m:5m])';
const queryLines = query.split('\n');
expect(validateQuery(query, query, queryLines, parser)).toEqual({
errors: [],
warnings: [
{ endColumn: 32, endLineNumber: 1, issue: warningTypes.SubqueryExpr, startColumn: 6, startLineNumber: 1 },
],
});
});
test('Warns agains queries with multiple subqueries', () => {
const query = 'quantile_over_time(0.5, rate(http_requests_total[1m:1m]) [1m:1m])';
const queryLines = query.split('\n');
expect(validateQuery(query, query, queryLines, parser)).toEqual({
errors: [],
warnings: [
{ issue: warningTypes.SubqueryExpr, startColumn: 25, endColumn: 65, startLineNumber: 1, endLineNumber: 1 },
{ issue: warningTypes.SubqueryExpr, startColumn: 30, endColumn: 56, startLineNumber: 1, endLineNumber: 1 },
],
});
});
});

View File

@@ -1,179 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/validation.ts
import { type SyntaxNode } from '@lezer/common';
import { type LRParser } from '@lezer/lr';
// Although 0 isn't explicitly provided in the @grafana/lezer-logql library as the error node ID, it does appear to be the ID of error nodes within lezer.
const ErrorId = 0;
export const warningTypes: Record<string, string> = {
SubqueryExpr:
'This subquery may return only one data point, preventing rate/increase/delta calculations. Use a range at least twice the step size (e.g., [2x:x]).',
};
enum NodeType {
SubqueryExpr = 'SubqueryExpr',
Duration = 'NumberDurationLiteralInDurationContext',
}
interface ParserIssueBoundary {
startLineNumber: number;
startColumn: number;
endLineNumber: number;
endColumn: number;
issue: string;
}
interface ParseIssue {
text: string;
node: SyntaxNode;
}
/**
* Conceived to work in combination with the MonacoQueryField component.
* Given an original query, and it's interpolated version, it will return an array of ParserErrorBoundary
* objects containing nodes which are actual errors. The interpolated version (even with placeholder variables)
* is required because variables look like errors for Lezer.
* @internal
*/
export function validateQuery(
query: string,
interpolatedQuery: string,
queryLines: string[],
parser: LRParser
): { errors: ParserIssueBoundary[]; warnings: ParserIssueBoundary[] } {
if (!query) {
return { errors: [], warnings: [] };
}
/**
* To provide support to variable interpolation in query validation, we run the parser in the interpolated
* query. If there are errors there, we trace them back to the original unparsed query, so we can more
* accurately highlight the error in the query, since it's likely that the variable name and variable value
* have different lengths. With this, we also exclude irrelevant parser errors that are produced by
* lezer not understanding $variables and $__variables, which usually generate 2 or 3 error SyntaxNode.
*/
const { errors: interpolatedErrors, warnings: interpolatedWarnings } = parseQuery(interpolatedQuery, parser);
if (!interpolatedErrors.length && !interpolatedWarnings.length) {
return { errors: [], warnings: [] };
}
let parseErrors: ParseIssue[] = interpolatedErrors;
let parseWarnings: ParseIssue[] = interpolatedWarnings;
if (query !== interpolatedQuery) {
const { errors: queryErrors, warnings: queryWarnings } = parseQuery(query, parser);
parseErrors = interpolatedErrors.flatMap(
(interpolatedError) =>
queryErrors.filter((queryError) => interpolatedError.text === queryError.text) || interpolatedError
);
parseWarnings = interpolatedWarnings.flatMap(
(interpolatedWarning) =>
queryWarnings.filter((queryWarning) => interpolatedWarning.node.from === queryWarning.node.from) ||
interpolatedWarning
);
}
const errorBoundaries = parseErrors
.map((parseError) => findIssueBoundary(query, queryLines, parseError, 'error'))
.filter(isValidIssueBoundary);
const warningBoundaries = parseWarnings
.map((parseWarning) => findIssueBoundary(query, queryLines, parseWarning, 'warning'))
.filter(isValidIssueBoundary);
return {
errors: errorBoundaries,
warnings: warningBoundaries,
};
}
function parseQuery(query: string, parser: LRParser) {
const parseErrors: ParseIssue[] = [];
const parseWarnings: ParseIssue[] = [];
const tree = parser.parse(query);
tree.iterate({
enter: (nodeRef): false | void => {
if (nodeRef.type.id === ErrorId) {
const node = nodeRef.node;
parseErrors.push({ node: node, text: query.substring(node.from, node.to) });
}
if (nodeRef.type.name === NodeType.SubqueryExpr) {
const node = nodeRef.node;
const durations: string[] = [];
const children = node.getChildren(NodeType.Duration);
for (const child of children) {
durations.push(query.substring(child.from, child.to));
}
if (durations.length === 2 && durations[0] === durations[1]) {
parseWarnings.push({ node: node, text: query.substring(node.from, node.to) });
}
}
},
});
return { errors: parseErrors, warnings: parseWarnings };
}
function findIssueBoundary(
query: string,
queryLines: string[],
parseError: ParseIssue,
issueType: 'error' | 'warning'
): ParserIssueBoundary | null {
if (queryLines.length === 1) {
const isEmptyString = parseError.node.from === parseError.node.to;
const errorNode = isEmptyString && parseError.node.parent ? parseError.node.parent : parseError.node;
let issue: string;
if (issueType === 'error') {
issue = isEmptyString ? query.substring(errorNode.from, errorNode.to) : parseError.text;
} else {
issue = warningTypes[parseError.node.type.name];
}
return {
startLineNumber: 1,
startColumn: errorNode.from + 1,
endLineNumber: 1,
endColumn: errorNode.to + 1,
issue,
};
}
let startPos = 0,
endPos = 0;
for (let line = 0; line < queryLines.length; line++) {
endPos = startPos + queryLines[line].length;
if (parseError.node.from > endPos) {
startPos += queryLines[line].length + 1;
continue;
}
return {
startLineNumber: line + 1,
startColumn: parseError.node.from - startPos + 1,
endLineNumber: line + 1,
endColumn: parseError.node.to - startPos + 1,
issue: issueType === 'error' ? parseError.text : warningTypes[parseError.node.type.name],
};
}
return null;
}
function isValidIssueBoundary(boundary: ParserIssueBoundary | null): boundary is ParserIssueBoundary {
return boundary !== null;
}
export const placeHolderScopedVars = {
__interval: { text: '1s', value: '1s' },
__rate_interval: { text: '1s', value: '1s' },
__auto: { text: '1s', value: '1s' },
__interval_ms: { text: '1000', value: 1000 },
__range_ms: { text: '1000', value: 1000 },
__range_s: { text: '1', value: 1 },
__range: { text: '1s', value: '1s' },
};

View File

@@ -1,258 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) Celian Garcia and Augustin Husson @ Amadeus IT Group
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
'use strict';
// import { languages } from "monaco-editor";
// noinspection JSUnusedGlobalSymbols
export const languageConfiguration = {
// the default separators except `@$`
wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+\[{\]}\\|;:'",.<>\/?\s]+)/g,
// Not possible to make comments in PromQL syntax
comments: {
lineComment: '#',
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
{ open: '<', close: '>' },
],
folding: {},
};
// PromQL Aggregation Operators
// (https://prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators)
const aggregations = [
'sum',
'min',
'max',
'avg',
'group',
'stddev',
'stdvar',
'count',
'count_values',
'bottomk',
'topk',
'quantile',
];
// PromQL functions
// (https://prometheus.io/docs/prometheus/latest/querying/functions/)
const functions = [
'abs',
'absent',
'ceil',
'changes',
'clamp',
'clamp_max',
'clamp_min',
'day_of_month',
'day_of_week',
'days_in_month',
'delta',
'deriv',
'double_exponential_smoothing',
'exp',
'floor',
'histogram_quantile',
'histogram_avg',
'histogram_count',
'histogram_sum',
'histogram_fraction',
'histogram_stddev',
'histogram_stdvar',
// Renamed as DoubleExponentialSmoothing with Prometheus v3.x
// https://github.com/prometheus/prometheus/pull/14930
'holt_winters',
'hour',
'idelta',
'increase',
'info',
'irate',
'label_join',
'label_replace',
'ln',
'log2',
'log10',
'minute',
'month',
'predict_linear',
'rate',
'resets',
'round',
'scalar',
'sort',
'sort_desc',
'sqrt',
'time',
'timestamp',
'vector',
'year',
];
// PromQL specific functions: Aggregations over time
// (https://prometheus.io/docs/prometheus/latest/querying/functions/#aggregation_over_time)
const aggregationsOverTime = [];
for (let _i = 0, aggregations_1 = aggregations; _i < aggregations_1.length; _i++) {
let agg = aggregations_1[_i];
aggregationsOverTime.push(agg + '_over_time');
}
// PromQL vector matching + the by and without clauses
// (https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching)
const vectorMatching = ['on', 'ignoring', 'group_right', 'group_left', 'by', 'without'];
// Produce a regex matching elements : (elt1|elt2|...)
const vectorMatchingRegex =
'(' +
vectorMatching.reduce(function (prev, curr) {
return prev + '|' + curr;
}) +
')';
// PromQL Operators
// (https://prometheus.io/docs/prometheus/latest/querying/operators/)
const operators = ['+', '-', '*', '/', '%', '^', '==', '!=', '>', '<', '>=', '<=', 'and', 'or', 'unless'];
// PromQL offset modifier
// (https://prometheus.io/docs/prometheus/latest/querying/basics/#offset-modifier)
const offsetModifier = ['offset'];
// Merging all the keywords in one list
const keywords = aggregations
.concat(functions)
.concat(aggregationsOverTime)
.concat(vectorMatching)
.concat(offsetModifier);
// noinspection JSUnusedGlobalSymbols
export const language = {
ignoreCase: false,
defaultToken: '',
tokenPostfix: '.promql',
keywords: keywords,
operators: operators,
vectorMatching: vectorMatchingRegex,
// we include these common regular expressions
symbols: /[=><!~?:&|+\-*\/^%]+/,
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
digits: /\d+(_+\d+)*/,
octaldigits: /[0-7]+(_+[0-7]+)*/,
binarydigits: /[0-1]+(_+[0-1]+)*/,
hexdigits: /[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,
integersuffix: /(ll|LL|u|U|l|L)?(ll|LL|u|U|l|L)?/,
floatsuffix: /[fFlL]?/,
// The main tokenizer for our languages
tokenizer: {
root: [
// 'by', 'without' and vector matching
[/@vectorMatching\s*(?=\()/, 'type', '@clauses'],
// labels
[/[a-z_]\w*(?=\s*(=|!=|=~|!~))/, 'tag'],
// comments
[/(^#.*$)/, 'comment'],
// all keywords have the same color
[
/[a-zA-Z_]\w*/,
{
cases: {
'@keywords': 'type',
'@default': 'identifier',
},
},
],
// strings
[/"([^"\\]|\\.)*$/, 'string.invalid'],
[/'([^'\\]|\\.)*$/, 'string.invalid'],
[/"/, 'string', '@string_double'],
[/'/, 'string', '@string_single'],
[/`/, 'string', '@string_backtick'],
// whitespace
{ include: '@whitespace' },
// delimiters and operators
[/[{}()\[\]]/, '@brackets'],
[/[<>](?!@symbols)/, '@brackets'],
[
/@symbols/,
{
cases: {
'@operators': 'delimiter',
'@default': '',
},
},
],
// numbers
[/\d+[smhdwy]/, 'number'],
[/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/, 'number.float'],
[/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/, 'number.float'],
[/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/, 'number.hex'],
[/0[0-7']*[0-7](@integersuffix)/, 'number.octal'],
[/0[bB][0-1']*[0-1](@integersuffix)/, 'number.binary'],
[/\d[\d']*\d(@integersuffix)/, 'number'],
[/\d(@integersuffix)/, 'number'],
],
string_double: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, 'string', '@pop'],
],
string_single: [
[/[^\\']+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/'/, 'string', '@pop'],
],
string_backtick: [
[/[^\\`$]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/`/, 'string', '@pop'],
],
clauses: [
[/[^(,)]/, 'tag'],
[/\)/, 'identifier', '@pop'],
],
whitespace: [[/[ \t\r\n]+/, 'white']],
},
};
// noinspection JSUnusedGlobalSymbols
// export const completionItemProvider = {
// provideCompletionItems: function () {
// // To simplify, we made the choice to never create automatically the parenthesis behind keywords
// // It is because in PromQL, some keywords need parenthesis behind, some don't, some can have but it's optional.
// const suggestions = keywords.map(function (value) {
// return {
// label: value,
// kind: languages.CompletionItemKind.Keyword,
// insertText: value,
// insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet
// };
// });
// return { suggestions: suggestions };
// }
// };

View File

@@ -1,7 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/types.ts
import { type QueryEditorProps } from '@grafana/data';
import { type PrometheusDatasource } from '../datasource';
import { type PromOptions, type PromQuery } from '../types';
export type PromQueryEditorProps = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>;

View File

@@ -1,93 +0,0 @@
import { render } from '@testing-library/react';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { createDefaultConfigOptions } from '../test/mocks/datasource';
import { AlertingSettingsOverhaul } from './AlertingSettingsOverhaul';
describe(AlertingSettingsOverhaul.name, () => {
describe('Manage Alerts toggle', () => {
describe('when options.jsonData.manageAlerts is unset', () => {
it('uses the config default `true`', () => {
const options = createDefaultConfigOptions();
options.jsonData.manageAlerts = undefined;
config.defaultDatasourceManageAlertsUiToggle = true;
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const manageAlertsToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`
);
expect(manageAlertsToggle).toBeChecked();
});
it('uses the config default `false`', () => {
const options = createDefaultConfigOptions();
options.jsonData.manageAlerts = undefined;
config.defaultDatasourceManageAlertsUiToggle = false;
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const manageAlertsToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`
);
expect(manageAlertsToggle).not.toBeChecked();
});
});
describe('when options.jsonData.manageAlerts is set', () => {
it.each([true, false])('uses the manageAlerts value even when the config default is %s', (configDefault) => {
const options = createDefaultConfigOptions();
options.jsonData.manageAlerts = true;
config.defaultDatasourceManageAlertsUiToggle = configDefault;
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const manageAlertsToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`
);
expect(manageAlertsToggle).toBeChecked();
});
});
});
describe('Recording Rules Target toggle', () => {
describe('when options.jsonData.allowAsRecordingRulesTarget is unset', () => {
it('defaults to `true` (enabled)', () => {
const options = createDefaultConfigOptions();
options.jsonData.allowAsRecordingRulesTarget = undefined;
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const recordingRulesTargetToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.allowAsRecordingRulesTarget}`
);
expect(recordingRulesTargetToggle).toBeChecked();
});
});
describe('when options.jsonData.allowAsRecordingRulesTarget is set', () => {
it.each([true, false])('uses the allowAsRecordingRulesTarget value %s', (value) => {
const options = createDefaultConfigOptions();
options.jsonData.allowAsRecordingRulesTarget = value;
const { container } = render(<AlertingSettingsOverhaul onOptionsChange={() => {}} options={options} />);
const recordingRulesTargetToggle = container.querySelector(
`#${selectors.components.DataSource.Prometheus.configPage.allowAsRecordingRulesTarget}`
);
if (value) {
expect(recordingRulesTargetToggle).toBeChecked();
} else {
expect(recordingRulesTargetToggle).not.toBeChecked();
}
});
});
});
});

View File

@@ -1,109 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/AlertingSettingsOverhaul.tsx
import { cx } from '@emotion/css';
import type { JSX } from 'react';
import { type DataSourceJsonData, type DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { ConfigSubSection } from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import { InlineField, Switch, useTheme2 } from '@grafana/ui';
import { docsTip, overhaulStyles } from './shared/utils';
interface Props<T extends DataSourceJsonData>
extends Pick<DataSourcePluginOptionsEditorProps<T>, 'options' | 'onOptionsChange'> {}
interface AlertingConfig extends DataSourceJsonData {
manageAlerts?: boolean;
allowAsRecordingRulesTarget?: boolean;
}
export function AlertingSettingsOverhaul<T extends AlertingConfig>({
options,
onOptionsChange,
}: Props<T>): JSX.Element {
const theme = useTheme2();
// imported GrafanaTheme2 from @grafana/data does not match type of same from @grafana/ui
// @ts-ignore
const styles = overhaulStyles(theme);
return (
<ConfigSubSection
title={t('grafana-prometheus.configuration.alerting-settings-overhaul.title-alerting', 'Alerting')}
className={cx(styles.container, styles.alertingTop)}
>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<InlineField
labelWidth={30}
label={t(
'grafana-prometheus.configuration.alerting-settings-overhaul.label-manage-alerts-via-alerting-ui',
'Manage alerts via Alerting UI'
)}
disabled={options.readOnly}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.alerting-settings-overhaul.tooltip-manage-alerts-via-alerting-ui">
Manage alert rules for this data source. To manage other alerting resources, add an Alertmanager
data source.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
className={styles.switchField}
>
<Switch
value={options.jsonData.manageAlerts ?? config.defaultDatasourceManageAlertsUiToggle}
onChange={(event) =>
onOptionsChange({
...options,
jsonData: { ...options.jsonData, manageAlerts: event!.currentTarget.checked },
})
}
id={selectors.components.DataSource.Prometheus.configPage.manageAlerts}
/>
</InlineField>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineField
labelWidth={30}
label={t(
'grafana-prometheus.configuration.alerting-settings-overhaul.label-allow-as-recording-rules-target',
'Allow as recording rules target'
)}
disabled={options.readOnly}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.alerting-settings-overhaul.tooltip-allow-as-recording-rules-target">
Allow this data source to be selected as a target for writing recording rules.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
className={styles.switchField}
>
<Switch
value={
options.jsonData.allowAsRecordingRulesTarget ?? config.defaultAllowRecordingRulesTargetAlertsUiToggle
}
onChange={(event) =>
onOptionsChange({
...options,
jsonData: { ...options.jsonData, allowAsRecordingRulesTarget: event!.currentTarget.checked },
})
}
id={selectors.components.DataSource.Prometheus.configPage.allowAsRecordingRulesTarget}
/>
</InlineField>
</div>
</div>
</div>
</ConfigSubSection>
);
}

View File

@@ -1,93 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.test.tsx
import { FieldValidationMessage } from '@grafana/ui';
import { DURATION_REGEX, MULTIPLE_DURATION_REGEX } from '../constants';
import { validateInput } from './shared/utils';
const VALID_URL_REGEX = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
const error = <FieldValidationMessage>Value is not valid</FieldValidationMessage>;
// replaces promSettingsValidationEvents to display a <FieldValidationMessage> onBlur for duration input errors
describe('promSettings validateInput', () => {
it.each`
value | expected
${'1ms'} | ${true}
${'1M'} | ${true}
${'1w'} | ${true}
${'1d'} | ${true}
${'1h'} | ${true}
${'1m'} | ${true}
${'1s'} | ${true}
${'1y'} | ${true}
`(
"Single duration regex, when calling the rule with correct formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, DURATION_REGEX)).toBe(expected);
}
);
it.each`
value | expected
${'1M 2s'} | ${true}
${'1w 2d'} | ${true}
${'1d 2m'} | ${true}
${'1h 2m'} | ${true}
${'1m 2s'} | ${true}
`(
"Multiple duration regex, when calling the rule with correct formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, MULTIPLE_DURATION_REGEX)).toBe(expected);
}
);
it.each`
value | expected
${'1 ms'} | ${error}
${'1x'} | ${error}
${' '} | ${error}
${'w'} | ${error}
${'1.0s'} | ${error}
`(
"when calling the rule with incorrect formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, DURATION_REGEX)).toStrictEqual(expected);
}
);
it.each`
value | expected
${'frp://'} | ${error}
${'htp://'} | ${error}
${'httpss:??'} | ${error}
${'http@//'} | ${error}
${'http:||'} | ${error}
${'http://'} | ${error}
${'https://'} | ${error}
${'ftp://'} | ${error}
`(
"Url incorrect formatting, when calling the rule with correct formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, VALID_URL_REGEX)).toStrictEqual(expected);
}
);
it.each`
value | expected
${'ftp://example'} | ${true}
${'http://example'} | ${true}
${'https://example'} | ${true}
`(
"Url correct formatting, when calling the rule with correct formatted value: '$value' then result should be '$expected'",
({ value, expected }) => {
expect(validateInput(value, VALID_URL_REGEX)).toBe(expected);
}
);
it('should display a custom validation message', () => {
const invalidDuration = 'invalid';
const customMessage = 'This is invalid';
const errorWithCustomMessage = <FieldValidationMessage>{customMessage}</FieldValidationMessage>;
expect(validateInput(invalidDuration, DURATION_REGEX, customMessage)).toStrictEqual(errorWithCustomMessage);
});
});

View File

@@ -1,60 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx
import { type DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { ConfigSection, DataSourceDescription, AdvancedHttpSettings } from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import { Alert, useTheme2 } from '@grafana/ui';
import { type PromOptions } from '../types';
import { AlertingSettingsOverhaul } from './AlertingSettingsOverhaul';
import { DataSourceHttpSettingsOverhaul } from './DataSourceHttpSettingsOverhaul';
import { PromSettings } from './PromSettings';
import { overhaulStyles } from './shared/utils';
type PrometheusConfigProps = DataSourcePluginOptionsEditorProps<PromOptions>;
export const ConfigEditor = (props: PrometheusConfigProps) => {
const { options, onOptionsChange } = props;
const theme = useTheme2();
const styles = overhaulStyles(theme);
return (
<>
{options.access === 'direct' && (
<Alert title={t('grafana-prometheus.configuration.config-editor.title-error', 'Error')} severity="error">
<Trans i18nKey="grafana-prometheus.configuration.config-editor.browser-access-mode-error">
Browser access mode in the Prometheus data source is no longer available. Switch to server access mode.
</Trans>
</Alert>
)}
<DataSourceDescription
dataSourceName="Prometheus"
docsLink="https://grafana.com/docs/grafana/latest/datasources/prometheus/configure/"
/>
<hr className={`${styles.hrTopSpace} ${styles.hrBottomSpace}`} />
<DataSourceHttpSettingsOverhaul
options={options}
onOptionsChange={onOptionsChange}
secureSocksDSProxyEnabled={config.secureSocksDSProxyEnabled}
/>
<hr />
<ConfigSection
className={styles.advancedSettings}
title={t('grafana-prometheus.configuration.config-editor.title-advanced-settings', 'Advanced settings')}
description={t(
'grafana-prometheus.configuration.config-editor.description-advanced-settings',
'Additional settings are optional settings that can be configured for more control over your data source.'
)}
>
<AdvancedHttpSettings
className={styles.advancedHTTPSettingsMargin}
config={options}
onChange={onOptionsChange}
/>
<AlertingSettingsOverhaul<PromOptions> options={options} onOptionsChange={onOptionsChange} />
<PromSettings options={options} onOptionsChange={onOptionsChange} />
</ConfigSection>
</>
);
};

View File

@@ -1,111 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/DataSourceHttpSettingsOverhaul.tsx
import { type DataSourceSettings } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Auth, AuthMethod, ConnectionSettings, convertLegacyAuthProps } from '@grafana/plugin-ui';
import { SecureSocksProxySettings, useTheme2 } from '@grafana/ui';
import { type PromOptions } from '../types';
import { docsTip, overhaulStyles } from './shared/utils';
type DataSourceHttpSettingsProps = {
options: DataSourceSettings<PromOptions, {}>;
onOptionsChange: (options: DataSourceSettings<PromOptions, {}>) => void;
secureSocksDSProxyEnabled: boolean;
};
export const DataSourceHttpSettingsOverhaul = (props: DataSourceHttpSettingsProps) => {
const { options, onOptionsChange, secureSocksDSProxyEnabled } = props;
const newAuthProps = convertLegacyAuthProps({
config: options,
onChange: onOptionsChange,
});
const theme = useTheme2();
const styles = overhaulStyles(theme);
function returnSelectedMethod() {
return newAuthProps.selectedMethod;
}
// Do we need this switch anymore? Update the language.
let urlTooltip;
switch (options.access) {
case 'direct':
urlTooltip = (
<>
<Trans i18nKey="grafana-prometheus.configuration.data-source-http-settings-overhaul.tooltip-browser-access-mode">
Your access method is <em>Browser</em>, this means the URL needs to be accessible from the browser.
</Trans>
{docsTip()}
</>
);
break;
case 'proxy':
urlTooltip = (
<>
<Trans i18nKey="grafana-prometheus.configuration.data-source-http-settings-overhaul.tooltip-server-access-mode">
Your access method is <em>Server</em>, this means the URL needs to be accessible from the grafana
backend/server.
</Trans>
{docsTip()}
</>
);
break;
default:
urlTooltip = (
<>
<Trans
i18nKey="grafana-prometheus.configuration.data-source-http-settings-overhaul.tooltip-http-url"
values={{ exampleURL: 'http://your_server:8080' }}
>
Specify a complete HTTP URL (for example {'{{exampleURL}}'})
</Trans>
{docsTip()}
</>
);
}
return (
<>
<ConnectionSettings
urlPlaceholder="http://localhost:9090"
config={options}
onChange={onOptionsChange}
urlLabel="Prometheus server URL"
urlTooltip={urlTooltip}
/>
<hr className={`${styles.hrTopSpace} ${styles.hrBottomSpace}`} />
<Auth
// Reshaped legacy props
{...newAuthProps}
// Still need to call `onAuthMethodSelect` function from
// `newAuthProps` to store the legacy data correctly.
// Also make sure to store the data about your component
// being selected/unselected.
onAuthMethodSelect={(method) => {
onOptionsChange({
...options,
basicAuth: method === AuthMethod.BasicAuth,
withCredentials: method === AuthMethod.CrossSiteCredentials,
jsonData: {
...options.jsonData,
oauthPassThru: method === AuthMethod.OAuthForward,
},
});
}}
// If your method is selected pass its id to `selectedMethod`,
// otherwise pass the id from converted legacy data
selectedMethod={returnSelectedMethod()}
/>
<div className={styles.sectionBottomPadding} />
{secureSocksDSProxyEnabled && (
<>
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
<div className={styles.sectionBottomPadding} />
</>
)}
</>
);
};

View File

@@ -1,205 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/ExemplarSetting.tsx
import { useState } from 'react';
import { type DataSourceInstanceSettings } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { config, DataSourcePicker } from '@grafana/runtime';
import { Button, InlineField, Input, Switch, useTheme2 } from '@grafana/ui';
import { PROM_CONFIG_LABEL_WIDTH } from '../constants';
import { type ExemplarTraceIdDestination } from '../types';
import { docsTip, overhaulStyles } from './shared/utils';
type Props = {
value: ExemplarTraceIdDestination;
onChange: (value: ExemplarTraceIdDestination) => void;
onDelete: () => void;
disabled?: boolean;
};
export function ExemplarSetting({ value, onChange, onDelete, disabled }: Props) {
const [isInternalLink, setIsInternalLink] = useState(Boolean(value.datasourceUid));
const theme = useTheme2();
const styles = overhaulStyles(theme);
return (
<div className="gf-form-group">
<InlineField
label={t('grafana-prometheus.configuration.exemplar-setting.label-internal-link', 'Internal link')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
disabled={disabled}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.exemplar-setting.tooltip-internal-link">
Enable this option if you have an internal link. When enabled, this reveals the data source selector.
Select the backend tracing data store for your exemplar data.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
className={styles.switchField}
>
<>
<Switch
value={isInternalLink}
data-testid={selectors.components.DataSource.Prometheus.configPage.internalLinkSwitch}
onChange={(ev) => setIsInternalLink(ev.currentTarget.checked)}
/>
</>
</InlineField>
{isInternalLink ? (
<InlineField
label={t('grafana-prometheus.configuration.exemplar-setting.label-data-source', 'Data source')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.exemplar-setting.tooltip-data-source">
The data source the exemplar is going to navigate to.
</Trans>{' '}
{docsTip()}
</>
}
disabled={disabled}
interactive={true}
>
<DataSourcePicker
filter={
config.featureToggles.azureMonitorPrometheusExemplars
? undefined
: (ds) => ds.type !== 'grafana-azure-monitor-datasource'
}
tracing={true}
current={value.datasourceUid}
noDefault={true}
width={40}
onChange={(ds: DataSourceInstanceSettings) =>
onChange({
...value,
datasourceUid: ds.uid,
url: undefined,
})
}
/>
</InlineField>
) : (
<InlineField
label={t('grafana-prometheus.configuration.exemplar-setting.label-url', 'URL')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.exemplar-setting.tooltip-url">
The URL of the trace backend the user would go to see its trace
</Trans>{' '}
{docsTip()}
</>
}
disabled={disabled}
interactive={true}
>
<Input
placeholder={t(
'grafana-prometheus.configuration.exemplar-setting.placeholder-httpsexamplecomvalueraw',
'https://example.com/${__value.raw}'
)}
spellCheck={false}
width={40}
value={value.url}
onChange={(event) =>
onChange({
...value,
datasourceUid: undefined,
url: event.currentTarget.value,
})
}
/>
</InlineField>
)}
<InlineField
label={t('grafana-prometheus.configuration.exemplar-setting.label-url-label', 'URL Label')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.exemplar-setting.tooltip-url-label">
Use to override the button label on the exemplar traceID field.
</Trans>{' '}
{docsTip()}
</>
}
disabled={disabled}
interactive={true}
>
<Input
placeholder={t(
'grafana-prometheus.configuration.exemplar-setting.placeholder-go-to-examplecom',
'Go to example.com'
)}
spellCheck={false}
width={40}
value={value.urlDisplayLabel}
onChange={(event) =>
onChange({
...value,
urlDisplayLabel: event.currentTarget.value,
})
}
/>
</InlineField>
<InlineField
label={t('grafana-prometheus.configuration.exemplar-setting.label-label-name', 'Label name')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.exemplar-setting.tooltip-label-name">
The name of the field in the labels object that should be used to get the traceID.
</Trans>{' '}
{docsTip()}
</>
}
disabled={disabled}
interactive={true}
>
<Input
placeholder={t('grafana-prometheus.configuration.exemplar-setting.placeholder-trace-id', 'traceID')}
spellCheck={false}
width={40}
value={value.name}
onChange={(event) =>
onChange({
...value,
name: event.currentTarget.value,
})
}
/>
</InlineField>
{!disabled && (
<InlineField
label={t(
'grafana-prometheus.configuration.exemplar-setting.label-remove-exemplar-link',
'Remove exemplar link'
)}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
disabled={disabled}
>
<Button
variant="destructive"
aria-label={t(
'grafana-prometheus.configuration.exemplar-setting.title-remove-exemplar-link',
'Remove exemplar link'
)}
icon="times"
onClick={(event) => {
event.preventDefault();
onDelete();
}}
/>
</InlineField>
)}
</div>
);
}

View File

@@ -1,77 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/ExemplarsSettings.tsx
import { css } from '@emotion/css';
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { ConfigSubSection } from '@grafana/plugin-ui';
import { Button, useTheme2 } from '@grafana/ui';
import { type ExemplarTraceIdDestination } from '../types';
import { ExemplarSetting } from './ExemplarSetting';
import { overhaulStyles } from './shared/utils';
type Props = {
options?: ExemplarTraceIdDestination[];
onChange: (value: ExemplarTraceIdDestination[]) => void;
disabled?: boolean;
};
export function ExemplarsSettings({ options, onChange, disabled }: Props) {
const theme = useTheme2();
const styles = overhaulStyles(theme);
return (
<div className={styles.sectionBottomPadding}>
<ConfigSubSection
title={t('grafana-prometheus.configuration.exemplars-settings.title-exemplars', 'Exemplars')}
className={styles.container}
>
{options &&
options.map((option, index) => {
return (
<ExemplarSetting
key={index}
value={option}
onChange={(newField) => {
const newOptions = [...options];
newOptions.splice(index, 1, newField);
onChange(newOptions);
}}
onDelete={() => {
const newOptions = [...options];
newOptions.splice(index, 1);
onChange(newOptions);
}}
disabled={disabled}
/>
);
})}
{!disabled && (
<Button
variant="secondary"
data-testid={selectors.components.DataSource.Prometheus.configPage.exemplarsAddButton}
className={css({
marginBottom: '10px',
})}
icon="plus"
onClick={(event) => {
event.preventDefault();
const newOptions = [...(options || []), { name: 'traceID' }];
onChange(newOptions);
}}
>
<Trans i18nKey="grafana-prometheus.configuration.exemplars-settings.add">Add</Trans>
</Button>
)}
{disabled && !options && (
<i>
<Trans i18nKey="grafana-prometheus.configuration.exemplars-settings.no-exemplars-configurations">
No exemplars configurations
</Trans>
</i>
)}
</ConfigSubSection>
</div>
);
}

View File

@@ -1,100 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/PromFlavorVersions.ts
export const PromFlavorVersions: { [index: string]: Array<{ value?: string; label: string }> } = {
Prometheus: [
{ value: undefined, label: 'Please select' },
{ value: '2.0.0', label: '< 2.14.x' },
{ value: '2.14.0', label: '2.14.x' },
{ value: '2.15.0', label: '2.15.x' },
{ value: '2.16.0', label: '2.16.x' },
{ value: '2.17.0', label: '2.17.x' },
{ value: '2.18.0', label: '2.18.x' },
{ value: '2.19.0', label: '2.19.x' },
{ value: '2.20.0', label: '2.20.x' },
{ value: '2.21.0', label: '2.21.x' },
{ value: '2.22.0', label: '2.22.x' },
{ value: '2.23.0', label: '2.23.x' },
{ value: '2.24.0', label: '2.24.x' },
{ value: '2.25.0', label: '2.25.x' },
{ value: '2.26.0', label: '2.26.x' },
{ value: '2.27.0', label: '2.27.x' },
{ value: '2.28.0', label: '2.28.x' },
{ value: '2.29.0', label: '2.29.x' },
{ value: '2.30.0', label: '2.30.x' },
{ value: '2.31.0', label: '2.31.x' },
{ value: '2.32.0', label: '2.32.x' },
{ value: '2.33.0', label: '2.33.x' },
{ value: '2.34.0', label: '2.34.x' },
{ value: '2.35.0', label: '2.35.x' },
{ value: '2.36.0', label: '2.36.x' },
{ value: '2.37.0', label: '2.37.x' },
{ value: '2.38.0', label: '2.38.x' },
{ value: '2.39.0', label: '2.39.x' },
{ value: '2.40.0', label: '2.40.x' },
{ value: '2.41.0', label: '2.41.x' },
{ value: '2.42.0', label: '2.42.x' },
{ value: '2.43.0', label: '2.43.x' },
{ value: '2.44.0', label: '2.44.x' },
{ value: '2.45.0', label: '2.45.x' },
{ value: '2.46.0', label: '2.46.x' },
{ value: '2.47.0', label: '2.47.x' },
{ value: '2.48.0', label: '2.48.x' },
{ value: '2.49.0', label: '2.49.x' },
{ value: '2.50.0', label: '2.50.x' },
// This value will be returned for future versions of prometheus until we add new entries to this object
{ value: '2.50.1', label: '> 2.50.x' },
],
Mimir: [
{ value: undefined, label: 'Please select' },
{ value: '2.0.0', label: '2.0.x' },
{ value: '2.1.0', label: '2.1.x' },
{ value: '2.2.0', label: '2.2.x' },
{ value: '2.3.0', label: '2.3.x' },
{ value: '2.4.0', label: '2.4.x' },
{ value: '2.5.0', label: '2.5.x' },
{ value: '2.6.0', label: '2.6.x' },
{ value: '2.7.0', label: '2.7.x' },
{ value: '2.8.0', label: '2.8.x' },
{ value: '2.9.0', label: '2.9.x' },
{ value: '2.9.1', label: '> 2.9.x' },
],
Thanos: [
{ value: undefined, label: 'Please select' },
{ value: '0.0.0', label: '< 0.16.x' },
{ value: '0.16.0', label: '0.16.x' },
{ value: '0.17.0', label: '0.17.x' },
{ value: '0.18.0', label: '0.18.x' },
{ value: '0.19.0', label: '0.19.x' },
{ value: '0.20.0', label: '0.20.x' },
{ value: '0.21.0', label: '0.21.x' },
{ value: '0.22.0', label: '0.22.x' },
{ value: '0.23.0', label: '0.23.x' },
{ value: '0.24.0', label: '0.24.x' },
{ value: '0.25.0', label: '0.25.x' },
{ value: '0.26.0', label: '0.26.x' },
{ value: '0.27.0', label: '0.27.x' },
{ value: '0.28.0', label: '0.28.x' },
{ value: '0.29.0', label: '0.29.x' },
{ value: '0.30.0', label: '0.30.x' },
{ value: '0.31.0', label: '0.31.x' },
{ value: '0.31.1', label: '> 0.31.x' },
],
Cortex: [
{ value: undefined, label: 'Please select' },
{ value: '0.0.0', label: '< 1.0.0' },
{ value: '1.0.0', label: '1.0.0' },
{ value: '1.1.0', label: '1.1.x' },
{ value: '1.2.0', label: '1.2.x' },
{ value: '1.3.0', label: '1.3.x' },
{ value: '1.4.0', label: '1.4.x' },
{ value: '1.5.0', label: '1.5.x' },
{ value: '1.6.0', label: '1.6.x' },
{ value: '1.7.0', label: '1.7.x' },
{ value: '1.8.0', label: '1.8.x' },
{ value: '1.9.0', label: '1.9.x' },
{ value: '1.10.0', label: '1.10.x' },
{ value: '1.11.0', label: '1.11.x' },
{ value: '1.13.0', label: '1.13.x' },
{ value: '1.14.0', label: '> 1.13.x' },
],
};

View File

@@ -1,74 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx
import { render, screen } from '@testing-library/react';
import { type SyntheticEvent } from 'react';
import { type SelectableValue } from '@grafana/data';
import { createDefaultConfigOptions } from '../test/mocks/datasource';
import { getValueFromEventItem, PromSettings } from './PromSettings';
describe('PromSettings', () => {
describe('getValueFromEventItem', () => {
describe('when called with undefined', () => {
it('then it should return empty string', () => {
const result = getValueFromEventItem(
undefined as unknown as SyntheticEvent<HTMLInputElement> | SelectableValue<string>
);
expect(result).toEqual('');
});
});
describe('when called with an input event', () => {
it('then it should return value from currentTarget', () => {
const value = 'An input value';
const result = getValueFromEventItem({ currentTarget: { value } });
expect(result).toEqual(value);
});
});
describe('when called with a select event', () => {
it('then it should return value', () => {
const value = 'A select value';
const result = getValueFromEventItem({ value });
expect(result).toEqual(value);
});
});
});
describe('PromSettings component', () => {
const defaultProps = createDefaultConfigOptions();
it('should show POST httpMethod if no httpMethod', () => {
const options = defaultProps;
options.url = '';
options.jsonData.httpMethod = '';
render(<PromSettings onOptionsChange={() => {}} options={options} />);
expect(screen.getByText('POST')).toBeInTheDocument();
});
it('should show POST httpMethod if POST httpMethod is configured', () => {
const options = defaultProps;
options.url = 'test_url';
options.jsonData.httpMethod = 'POST';
render(<PromSettings onOptionsChange={() => {}} options={options} />);
expect(screen.getByText('POST')).toBeInTheDocument();
});
it('should show GET httpMethod if GET httpMethod is configured', () => {
const options = defaultProps;
options.url = 'test_url';
options.jsonData.httpMethod = 'GET';
render(<PromSettings onOptionsChange={() => {}} options={options} />);
expect(screen.getByText('GET')).toBeInTheDocument();
});
it('should have a series endpoint configuration element', () => {
const options = defaultProps;
render(<PromSettings onOptionsChange={() => {}} options={options} />);
expect(screen.getByText('Use series endpoint')).toBeInTheDocument();
});
});
});

View File

@@ -1,670 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx
import { type SyntheticEvent, useState } from 'react';
import {
type DataSourcePluginOptionsEditorProps,
type DataSourceSettings,
onUpdateDatasourceJsonDataOptionChecked,
type SelectableValue,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { ConfigSubSection } from '@grafana/plugin-ui';
import { Box, InlineField, Input, Select, Stack, Switch, TextLink, useTheme2 } from '@grafana/ui';
import {
DEFAULT_SERIES_LIMIT,
DURATION_REGEX,
durationError,
MULTIPLE_DURATION_REGEX,
NON_NEGATIVE_INTEGER_REGEX,
PROM_CONFIG_LABEL_WIDTH,
seriesLimitError,
} from '../constants';
import { QueryEditorMode } from '../querybuilder/shared/types';
import { defaultPrometheusQueryOverlapWindow } from '../querycache/QueryCache';
import { PromApplication, PrometheusCacheLevel, type PromOptions } from '../types';
import { ExemplarsSettings } from './ExemplarsSettings';
import { PromFlavorVersions } from './PromFlavorVersions';
import { docsTip, overhaulStyles, validateInput } from './shared/utils';
type Props = Pick<DataSourcePluginOptionsEditorProps<PromOptions>, 'options' | 'onOptionsChange'> & {
/** Hide the Prometheus type and version dropdowns */
hidePrometheusTypeVersion?: boolean;
/** Hide the Exemplars settings section */
hideExemplars?: boolean;
};
const httpOptions = [
{ value: 'POST', label: 'POST' },
{ value: 'GET', label: 'GET' },
];
const cacheValueOptions = [
{ value: PrometheusCacheLevel.Low, label: 'Low' },
{ value: PrometheusCacheLevel.Medium, label: 'Medium' },
{ value: PrometheusCacheLevel.High, label: 'High' },
{ value: PrometheusCacheLevel.None, label: 'None' },
];
type PrometheusSelectItemsType = Array<{ value: PromApplication; label: PromApplication }>;
type ValidDuration = {
timeInterval: string;
queryTimeout: string;
incrementalQueryOverlapWindow: string;
};
const prometheusFlavorSelectItems: PrometheusSelectItemsType = [
{ value: PromApplication.Prometheus, label: PromApplication.Prometheus },
{ value: PromApplication.Cortex, label: PromApplication.Cortex },
{ value: PromApplication.Mimir, label: PromApplication.Mimir },
{ value: PromApplication.Thanos, label: PromApplication.Thanos },
];
const getOptionsWithDefaults = (options: DataSourceSettings<PromOptions>) => {
if (options.jsonData.httpMethod) {
return options;
}
// We are explicitly adding httpMethod so, it is correctly displayed in dropdown.
// This way, it is more predictable for users.
return { ...options, jsonData: { ...options.jsonData, httpMethod: 'POST' } };
};
export const PromSettings = (props: Props) => {
const theme = useTheme2();
const styles = overhaulStyles(theme);
const { onOptionsChange, hidePrometheusTypeVersion, hideExemplars } = props;
const editorOptions = [
{
value: QueryEditorMode.Builder,
label: t('grafana-prometheus.configuration.prom-settings.editor-options.label-builder', 'Builder'),
},
{
value: QueryEditorMode.Code,
label: t('grafana-prometheus.configuration.prom-settings.editor-options.label-code', 'Code'),
},
];
const optionsWithDefaults = getOptionsWithDefaults(props.options);
const [validDuration, updateValidDuration] = useState<ValidDuration>({
timeInterval: '',
queryTimeout: '',
incrementalQueryOverlapWindow: '',
});
const [seriesLimit, setSeriesLimit] = useState<string>(
optionsWithDefaults.jsonData.seriesLimit?.toString() || `${DEFAULT_SERIES_LIMIT}`
);
return (
<>
<ConfigSubSection
title={t('grafana-prometheus.configuration.prom-settings.title-interval-behaviour', 'Interval behaviour')}
className={styles.container}
>
<Box marginBottom={5}>
<Stack direction="column" gap={0.5}>
{/* Scrape interval */}
<InlineField
label={t('grafana-prometheus.configuration.prom-settings.label-scrape-interval', 'Scrape interval')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans
i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-scrape-interval"
values={{ default: '15s' }}
>
This interval is how frequently Prometheus scrapes targets. Set this to the typical scrape and
evaluation interval configured in your Prometheus config file. If you set this to a greater value
than your Prometheus config file interval, Grafana will evaluate the data according to this interval
and you will see less data points. Defaults to {'{{default}}'}.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
>
<>
<Input
className="width-20"
value={optionsWithDefaults.jsonData.timeInterval}
spellCheck={false}
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="15s"
onChange={onChangeHandler('timeInterval', optionsWithDefaults, onOptionsChange)}
onBlur={(e) =>
updateValidDuration({
...validDuration,
timeInterval: e.currentTarget.value,
})
}
data-testid={selectors.components.DataSource.Prometheus.configPage.scrapeInterval}
/>
{validateInput(validDuration.timeInterval, DURATION_REGEX, durationError)}
</>
</InlineField>
{/* Query Timeout */}
<InlineField
label={t('grafana-prometheus.configuration.prom-settings.label-query-timeout', 'Query timeout')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-query-timeout">
Set the Prometheus query timeout.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
>
<>
<Input
className="width-20"
value={optionsWithDefaults.jsonData.queryTimeout}
onChange={onChangeHandler('queryTimeout', optionsWithDefaults, onOptionsChange)}
spellCheck={false}
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="60s"
onBlur={(e) =>
updateValidDuration({
...validDuration,
queryTimeout: e.currentTarget.value,
})
}
data-testid={selectors.components.DataSource.Prometheus.configPage.queryTimeout}
/>
{validateInput(validDuration.queryTimeout, DURATION_REGEX, durationError)}
</>
</InlineField>
</Stack>
</Box>
</ConfigSubSection>
<ConfigSubSection
title={t('grafana-prometheus.configuration.prom-settings.title-query-editor', 'Query editor')}
className={styles.container}
>
<Box marginBottom={5}>
<Stack direction="column" gap={0.5}>
<InlineField
label={t('grafana-prometheus.configuration.prom-settings.label-default-editor', 'Default editor')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-default-editor">
Set default editor option for all users of this data source.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
>
<Select
aria-label={t(
'grafana-prometheus.configuration.prom-settings.aria-label-default-editor',
'Default Editor (Code or Builder)'
)}
options={editorOptions}
value={
editorOptions.find((o) => o.value === optionsWithDefaults.jsonData.defaultEditor) ??
editorOptions.find((o) => o.value === QueryEditorMode.Builder)
}
onChange={onChangeHandler('defaultEditor', optionsWithDefaults, onOptionsChange)}
width={40}
data-testid={selectors.components.DataSource.Prometheus.configPage.defaultEditor}
/>
</InlineField>
<InlineField
labelWidth={PROM_CONFIG_LABEL_WIDTH}
label={t(
'grafana-prometheus.configuration.prom-settings.label-disable-metrics-lookup',
'Disable metrics lookup'
)}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-disable-metrics-lookup">
Checking this option will disable the metrics chooser and metric/label support in the query
field&apos;s autocomplete. This helps if you have performance issues with bigger Prometheus
instances.{' '}
</Trans>
{docsTip()}
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
className={styles.switchField}
>
<Switch
value={optionsWithDefaults.jsonData.disableMetricsLookup ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'disableMetricsLookup')}
id={selectors.components.DataSource.Prometheus.configPage.disableMetricLookup}
/>
</InlineField>
</Stack>
</Box>
</ConfigSubSection>
<ConfigSubSection
title={t('grafana-prometheus.configuration.prom-settings.title-performance', 'Performance')}
className={styles.container}
>
{!hidePrometheusTypeVersion &&
!optionsWithDefaults.jsonData.prometheusType &&
!optionsWithDefaults.jsonData.prometheusVersion &&
optionsWithDefaults.readOnly && (
<div className={styles.versionMargin}>
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.more-info">
For more information on configuring prometheus type and version in data sources, see the{' '}
<TextLink external href="https://grafana.com/docs/grafana/latest/administration/provisioning/">
provisioning documentation
</TextLink>
.
</Trans>
</div>
)}
<Box marginBottom={5}>
<Stack direction="column" gap={0.5}>
{!hidePrometheusTypeVersion && (
<>
<InlineField
label={t('grafana-prometheus.configuration.prom-settings.label-prometheus-type', 'Prometheus type')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
{/* , and attempt to detect the version */}
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-prometheus-type">
Set this to the type of your prometheus database, e.g. Prometheus, Cortex, Mimir or Thanos.
Changing this field will save your current settings. Certain types of Prometheus supports or
does not support various APIs. For example, some types support regex matching for label queries
to improve performance. Some types have an API for metadata. If you set this incorrectly you may
experience odd behavior when querying metrics and labels. Please check your Prometheus
documentation to ensure you enter the correct type.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
>
<Select
aria-label={t(
'grafana-prometheus.configuration.prom-settings.aria-label-prometheus-type',
'Prometheus type'
)}
options={prometheusFlavorSelectItems}
value={prometheusFlavorSelectItems.find(
(o) => o.value === optionsWithDefaults.jsonData.prometheusType
)}
onChange={onChangeHandler('prometheusType', optionsWithDefaults, onOptionsChange)}
width={40}
data-testid={selectors.components.DataSource.Prometheus.configPage.prometheusType}
/>
</InlineField>
{optionsWithDefaults.jsonData.prometheusType && (
<InlineField
label={t(
'grafana-prometheus.configuration.prom-settings.label-prom-type-version',
'{{promType}} version',
{
promType: optionsWithDefaults.jsonData.prometheusType,
}
)}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans
i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-prom-type-version"
values={{ promType: optionsWithDefaults.jsonData.prometheusType }}
>
Use this to set the version of your {'{{promType}}'} instance if it is not automatically
configured.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
>
<Select
aria-label={t(
'grafana-prometheus.configuration.prom-settings.aria-label-prom-type-type',
'{{promType}} type',
{
promType: optionsWithDefaults.jsonData.prometheusType,
}
)}
options={PromFlavorVersions[optionsWithDefaults.jsonData.prometheusType]}
value={PromFlavorVersions[optionsWithDefaults.jsonData.prometheusType]?.find(
(o) => o.value === optionsWithDefaults.jsonData.prometheusVersion
)}
onChange={onChangeHandler('prometheusVersion', optionsWithDefaults, onOptionsChange)}
width={40}
data-testid={selectors.components.DataSource.Prometheus.configPage.prometheusVersion}
/>
</InlineField>
)}
</>
)}
<InlineField
label={t('grafana-prometheus.configuration.prom-settings.label-cache-level', 'Cache level')}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-cache-level">
Sets the browser caching level for editor queries. Higher cache settings are recommended for high
cardinality data sources.
</Trans>
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
>
<Select
width={40}
onChange={onChangeHandler('cacheLevel', optionsWithDefaults, onOptionsChange)}
options={cacheValueOptions}
value={
cacheValueOptions.find((o) => o.value === optionsWithDefaults.jsonData.cacheLevel) ??
PrometheusCacheLevel.Low
}
data-testid={selectors.components.DataSource.Prometheus.configPage.cacheLevel}
/>
</InlineField>
<InlineField
label={t(
'grafana-prometheus.configuration.prom-settings.label-incremental-querying-beta',
'Incremental querying (beta)'
)}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-incremental-querying-beta">
This feature will change the default behavior of relative queries to always request fresh data from
the prometheus instance, instead query results will be cached, and only new records are requested.
Turn this on to decrease database and network load.
</Trans>
</>
}
interactive={true}
className={styles.switchField}
disabled={optionsWithDefaults.readOnly}
>
<Switch
value={optionsWithDefaults.jsonData.incrementalQuerying ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'incrementalQuerying')}
id={selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}
/>
</InlineField>
{optionsWithDefaults.jsonData.incrementalQuerying && (
<InlineField
label={t(
'grafana-prometheus.configuration.prom-settings.label-query-overlap-window',
'Query overlap window'
)}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans
i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-query-overlap-window"
values={{
example1: '10m',
example2: '120s',
example3: '0s',
default: '10m',
}}
>
Set a duration like {'{{example1}}'} or {'{{example2}}'} or {'{{example3}}'}. Default of{' '}
{'{{default}}'}. This duration will be added to the duration of each incremental request.
</Trans>
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
>
<>
<Input
onBlur={(e) =>
updateValidDuration({
...validDuration,
incrementalQueryOverlapWindow: e.currentTarget.value,
})
}
className="width-20"
value={
optionsWithDefaults.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow
}
onChange={onChangeHandler('incrementalQueryOverlapWindow', optionsWithDefaults, onOptionsChange)}
spellCheck={false}
data-testid={selectors.components.DataSource.Prometheus.configPage.queryOverlapWindow}
/>
{validateInput(validDuration.incrementalQueryOverlapWindow, MULTIPLE_DURATION_REGEX, durationError)}
</>
</InlineField>
)}
<InlineField
label={t(
'grafana-prometheus.configuration.prom-settings.label-disable-recording-rules-beta',
'Disable recording rules (beta)'
)}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-disable-recording-rules-beta">
This feature will disable recording rules. Turn this on to improve dashboard performance
</Trans>
</>
}
interactive={true}
className={styles.switchField}
disabled={optionsWithDefaults.readOnly}
>
<Switch
value={optionsWithDefaults.jsonData.disableRecordingRules ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'disableRecordingRules')}
id={selectors.components.DataSource.Prometheus.configPage.disableRecordingRules}
/>
</InlineField>
</Stack>
</Box>
</ConfigSubSection>
<ConfigSubSection
title={t('grafana-prometheus.configuration.prom-settings.title-other', 'Other')}
className={styles.container}
>
<Box marginBottom={5}>
<Stack direction="column" gap={0.5}>
<InlineField
label={t(
'grafana-prometheus.configuration.prom-settings.label-custom-query-parameters',
'Custom query parameters'
)}
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans
i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-custom-query-parameters"
values={{
example1: 'timeout',
example2: 'partial_response',
example3: 'dedup',
example4: 'max_source_resolution',
concatenationChar: '&',
}}
>
Add custom parameters to the Prometheus query URL. For example {'{{example1}}'}, {'{{example2}}'},{' '}
{'{{example3}}'}, or
{'{{example4}}'}. Multiple parameters should be concatenated together with {'{{concatenationChar}}'}
.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
>
<Input
className="width-20"
value={optionsWithDefaults.jsonData.customQueryParameters}
onChange={onChangeHandler('customQueryParameters', optionsWithDefaults, onOptionsChange)}
spellCheck={false}
placeholder={t(
'grafana-prometheus.configuration.prom-settings.placeholder-example-maxsourceresolutionmtimeout',
'Example: {{example}}',
{ example: 'max_source_resolution=5m&timeout=10' }
)}
data-testid={selectors.components.DataSource.Prometheus.configPage.customQueryParameters}
/>
</InlineField>
{/* HTTP Method */}
<InlineField
labelWidth={PROM_CONFIG_LABEL_WIDTH}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-http-method">
You can use either POST or GET HTTP method to query your Prometheus data source. POST is the
recommended method as it allows bigger queries. Change this to GET if you have a Prometheus version
older than 2.1 or if POST requests are restricted in your network.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
label={t('grafana-prometheus.configuration.prom-settings.label-http-method', 'HTTP method')}
disabled={optionsWithDefaults.readOnly}
>
<Select
width={40}
aria-label={t(
'grafana-prometheus.configuration.prom-settings.aria-label-select-http-method',
'Select HTTP method'
)}
options={httpOptions}
value={httpOptions.find((o) => o.value === optionsWithDefaults.jsonData.httpMethod)}
onChange={onChangeHandler('httpMethod', optionsWithDefaults, onOptionsChange)}
data-testid={selectors.components.DataSource.Prometheus.configPage.httpMethod}
/>
</InlineField>
<InlineField
labelWidth={PROM_CONFIG_LABEL_WIDTH}
label={t('grafana-prometheus.configuration.prom-settings.label-series-limit', 'Series limit')}
tooltip={
<>
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-series-limit">
The limit applies to all resources (metrics, labels, and values) for both endpoints (series and
labels). Leave the field empty to use the default limit (40000). Set to 0 to disable the limit and
fetch everything this may cause performance issues. Default limit is 40000.
</Trans>
{docsTip()}
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
>
<>
<Input
className="width-20"
value={seriesLimit}
spellCheck={false}
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="40000"
onChange={(event: { currentTarget: { value: string } }) => {
setSeriesLimit(event.currentTarget.value);
onOptionsChange({
...optionsWithDefaults,
jsonData: {
...optionsWithDefaults.jsonData,
seriesLimit: parseInt(event.currentTarget.value, 10),
},
});
}}
onBlur={(e) => validateInput(e.currentTarget.value, NON_NEGATIVE_INTEGER_REGEX, seriesLimitError)}
data-testid={selectors.components.DataSource.Prometheus.configPage.seriesLimit}
/>
{validateInput(seriesLimit, NON_NEGATIVE_INTEGER_REGEX, seriesLimitError)}
</>
</InlineField>
<InlineField
labelWidth={PROM_CONFIG_LABEL_WIDTH}
label={t(
'grafana-prometheus.configuration.prom-settings.label-use-series-endpoint',
'Use series endpoint'
)}
tooltip={
<>
<Trans
i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-use-series-endpoint"
values={{ exampleParameter: 'match[]' }}
>
Checking this option will favor the series endpoint with {'{{exampleParameter}}'} parameter over the
label values endpoint with {'{{exampleParameter}}'} parameter. While the label values endpoint is
considered more performant, some users may prefer the series because it has a POST method while the
label values endpoint only has a GET method.
</Trans>{' '}
{docsTip()}
</>
}
interactive={true}
disabled={optionsWithDefaults.readOnly}
className={styles.switchField}
>
<Switch
value={optionsWithDefaults.jsonData.seriesEndpoint ?? false}
onChange={onUpdateDatasourceJsonDataOptionChecked(props, 'seriesEndpoint')}
/>
</InlineField>
</Stack>
</Box>
</ConfigSubSection>
{!hideExemplars && (
<ExemplarsSettings
options={optionsWithDefaults.jsonData.exemplarTraceIdDestinations}
onChange={(exemplarOptions) =>
updateDatasourcePluginJsonDataOption(
{ onOptionsChange, options: optionsWithDefaults },
'exemplarTraceIdDestinations',
exemplarOptions
)
}
disabled={optionsWithDefaults.readOnly}
/>
)}
</>
);
};
export const getValueFromEventItem = (eventItem: SyntheticEvent<HTMLInputElement> | SelectableValue<string>) => {
if (!eventItem) {
return '';
}
if ('currentTarget' in eventItem) {
return eventItem.currentTarget.value;
}
return eventItem.value;
};
const onChangeHandler =
(key: keyof PromOptions, options: Props['options'], onOptionsChange: Props['onOptionsChange']) =>
(eventItem: SyntheticEvent<HTMLInputElement> | SelectableValue<string>) => {
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
[key]: getValueFromEventItem(eventItem),
},
});
};

View File

@@ -1,128 +0,0 @@
import { css } from '@emotion/css';
import type { JSX } from 'react';
import { type GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { FieldValidationMessage, TextLink } from '@grafana/ui';
/**
* Use this to return a url in a tooltip in a field. Don't forget to make the field interactive to be able to click on the tooltip
* @param url
* @returns
*/
export function docsTip(url?: string) {
const docsUrl = 'https://grafana.com/docs/grafana/latest/datasources/prometheus/configure/';
return (
<TextLink href={url ? url : docsUrl} external>
<Trans i18nKey="grafana-prometheus.configuration.docs-tip.visit-docs-for-more-details-here">
Visit docs for more details here.
</Trans>
</TextLink>
);
}
export const validateInput = (
input: string,
pattern: string | RegExp,
errorMessage?: string
): boolean | JSX.Element => {
const defaultErrorMessage = 'Value is not valid';
const inputTooLongErrorMessage = 'Input is too long';
const validationTimeoutErrorMessage = 'Validation timeout - input too complex';
const invalidValidationPatternErrorMessage = 'Invalid validation pattern';
const MAX_INPUT_LENGTH = 1000; // Reasonable limit for most validation cases
// Early return if no input
if (!input) {
return true;
}
// Check input length
if (input.length > MAX_INPUT_LENGTH) {
return <FieldValidationMessage>{inputTooLongErrorMessage}</FieldValidationMessage>;
}
try {
// Convert string pattern to RegExp if needed
let regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
// Ensure pattern is properly anchored to prevent catastrophic backtracking
if (typeof pattern === 'string' && !pattern.startsWith('^') && !pattern.endsWith('$')) {
regex = new RegExp(`^${pattern}$`);
}
// Add timeout to prevent ReDoS
const timeout = 100; // 100ms timeout
const startTime = Date.now();
const isValid = regex.test(input);
// Check if execution took too long
if (Date.now() - startTime > timeout) {
return <FieldValidationMessage>{validationTimeoutErrorMessage}</FieldValidationMessage>;
}
if (!isValid) {
return <FieldValidationMessage>{errorMessage || defaultErrorMessage}</FieldValidationMessage>;
}
return true;
} catch (error) {
return <FieldValidationMessage>{invalidValidationPatternErrorMessage}</FieldValidationMessage>;
}
};
export function overhaulStyles(theme: GrafanaTheme2) {
return {
additionalSettings: css({
marginBottom: '25px',
}),
secondaryGrey: css({
color: theme.colors.secondary.text,
opacity: '65%',
}),
inlineError: css({
margin: '0px 0px 4px 245px',
}),
switchField: css({
alignItems: 'center',
}),
sectionHeaderPadding: css({
paddingTop: '32px',
}),
sectionBottomPadding: css({
paddingBottom: '28px',
}),
subsectionText: css({
fontSize: '12px',
}),
hrBottomSpace: css({
marginBottom: '56px',
}),
hrTopSpace: css({
marginTop: '50px',
}),
textUnderline: css({
textDecoration: 'underline',
}),
versionMargin: css({
marginBottom: '12px',
}),
advancedHTTPSettingsMargin: css({
margin: '24px 0 8px 0',
}),
advancedSettings: css({
paddingTop: '32px',
}),
alertingTop: css({
marginTop: '40px !important',
}),
overhaulPageHeading: css({
fontWeight: 400,
}),
container: css({
maxwidth: 578,
}),
};
}

View File

@@ -1,43 +0,0 @@
// Max number of items (metrics, labels, values) that we display as suggestions. Prevents from running out of memory.
export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000;
export const PROM_CONFIG_LABEL_WIDTH = 30;
export const LIST_ITEM_SIZE = 25;
export const LAST_USED_LABELS_KEY = 'grafana.datasources.prometheus.browser.labels';
export const DURATION_REGEX = /^$|^\d+(ms|[Mwdhmsy])$/;
export const MULTIPLE_DURATION_REGEX = /(\d+)(.+)/;
export const NON_NEGATIVE_INTEGER_REGEX = /^(0|[1-9]\d*)(\.\d+)?(e\+?\d+)?$/; // non-negative integers, including scientific notation
export const EMPTY_SELECTOR = '{}';
export const DEFAULT_SERIES_LIMIT = 40000;
export const DEFAULT_COMPLETION_LIMIT = 1000;
/**
* Only for /series endpoint. Don't use this anywhere else as it cause an expensive query
*/
export const MATCH_ALL_LABELS = '{__name__!=""}';
export const METRIC_LABEL = '__name__';
export const durationError = 'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s';
export const seriesLimitError =
'Value is not valid, you can use only numbers or leave it empty to use default limit or set 0 to have no limit.';
export const InstantQueryRefIdIndex = '-Instant';
export const GET_AND_POST_METADATA_ENDPOINTS = [
'api/v1/query',
'api/v1/query_range',
'api/v1/series',
'api/v1/labels',
'suggestions',
];

View File

@@ -1,49 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/dataquery.ts
import { type Scope, type ScopeSpec, type ScopeSpecFilter } from '@grafana/data';
import type * as common from '@grafana/schema';
export enum QueryEditorMode {
Builder = 'builder',
Code = 'code',
}
export type PromQueryFormat = 'time_series' | 'table' | 'heatmap';
export interface Prometheus extends common.DataQuery {
/**
* Specifies which editor is being used to prepare the query. It can be "code" or "builder"
*/
editorMode?: QueryEditorMode;
/**
* Execute an additional query to identify interesting raw samples relevant for the given expr
*/
exemplar?: boolean;
/**
* The actual expression/query that will be evaluated by Prometheus
*/
expr: string;
/**
* Query format to determine how to display data points in panel. It can be "time_series", "table", "heatmap"
*/
format?: PromQueryFormat;
/**
* Returns only the latest value that Prometheus has scraped for the requested time series
*/
instant?: boolean;
/**
* @deprecated Used to specify how many times to divide max data points by. We use max data points under query options
* See https://github.com/grafana/grafana/issues/48081
*/
intervalFactor?: number;
/**
* Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname
*/
legendFormat?: string;
/**
* Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series
*/
range?: boolean;
scopes?: Array<ScopeSpec & Pick<Scope['metadata'], 'name'>>;
adhocFilters?: ScopeSpecFilter[];
groupByKeys?: string[];
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,937 +0,0 @@
import { defaults } from 'lodash';
import { tz } from 'moment-timezone';
import { lastValueFrom, type Observable, throwError } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { gte } from 'semver';
import {
type AbstractQuery,
type AdHocVariableFilter,
CoreApp,
type CustomVariableModel,
type DataQueryRequest,
type DataQueryResponse,
type DataSourceGetTagKeysOptions,
type DataSourceGetTagValuesOptions,
type DataSourceInstanceSettings,
type DataSourceWithQueryExportSupport,
type DataSourceWithQueryImportSupport,
dateTime,
getDefaultTimeRange,
type LegacyMetricFindQueryOptions,
type MetricFindValue,
type QueryFixAction,
type QueryVariableModel,
rangeUtil,
type ScopedVars,
scopeFilterOperatorMap,
type ScopeSpecFilter,
type TimeRange,
} from '@grafana/data';
import {
type BackendSrvRequest,
config,
DataSourceWithBackend,
type FetchResponse,
getBackendSrv,
getTemplateSrv,
isFetchError,
type TemplateSrv,
} from '@grafana/runtime';
import { addLabelToQuery } from './add_label_to_query';
import { PrometheusAnnotationSupport } from './annotations';
import { DEFAULT_SERIES_LIMIT, GET_AND_POST_METADATA_ENDPOINTS, InstantQueryRefIdIndex } from './constants';
import { interpolateQueryExpr, prometheusRegularEscape } from './escaping';
import {
exportToAbstractQuery,
importFromAbstractQuery,
populateMatchParamsFromQueries,
PrometheusLanguageProvider,
type PrometheusLanguageProviderInterface,
} from './language_provider';
import { expandRecordingRules, getPrometheusTime, getRangeSnapInterval } from './language_utils';
import { PrometheusMetricFindQuery } from './metric_find_query';
import { getQueryHints } from './query_hints';
import { renderLabelsWithoutBrackets } from './querybuilder/shared/rendering/labels';
import { type QueryBuilderLabelFilter, type QueryEditorMode } from './querybuilder/shared/types';
import { type CacheRequestInfo, defaultPrometheusQueryOverlapWindow, QueryCache } from './querycache/QueryCache';
import { transformV2 } from './result_transformer';
import { trackQuery } from './tracking';
import {
type ExemplarTraceIdDestination,
PromApplication,
PrometheusCacheLevel,
type PromOptions,
type PromQuery,
type PromQueryRequest,
type RawRecordingRules,
type RuleQueryMapping,
} from './types';
import { utf8Support, wrapUtf8Filters } from './utf8_support';
import { PrometheusVariableSupport } from './variables';
export class PrometheusDatasource
extends DataSourceWithBackend<PromQuery, PromOptions>
implements DataSourceWithQueryImportSupport<PromQuery>, DataSourceWithQueryExportSupport<PromQuery>
{
access: 'direct' | 'proxy';
basicAuth: any;
cache: QueryCache<PromQuery>;
cacheLevel: PrometheusCacheLevel;
customQueryParameters: URLSearchParams;
datasourceConfigurationPrometheusFlavor?: PromApplication;
datasourceConfigurationPrometheusVersion?: string;
disableRecordingRules: boolean;
exemplarTraceIdDestinations: ExemplarTraceIdDestination[] | undefined;
exemplarsAvailable: boolean;
hasIncrementalQuery: boolean;
httpMethod: string;
interval: string;
languageProvider: PrometheusLanguageProviderInterface;
lookupsDisabled: boolean;
ruleMappings: RuleQueryMapping;
seriesEndpoint: boolean;
seriesLimit: number;
type: string;
url: string;
withCredentials: boolean;
defaultEditor?: QueryEditorMode;
constructor(
public readonly instanceSettings: DataSourceInstanceSettings<PromOptions>,
private readonly templateSrv: TemplateSrv = getTemplateSrv(),
languageProvider?: PrometheusLanguageProviderInterface
) {
super(instanceSettings);
// DATASOURCE CONFIGURATION PROPERTIES
this.access = instanceSettings.access;
this.basicAuth = instanceSettings.basicAuth;
this.cache = new QueryCache({
getTargetSignature: this.getPrometheusTargetSignature.bind(this),
overlapString: instanceSettings.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow,
applyInterpolation: this.interpolateString.bind(this),
});
this.cacheLevel = instanceSettings.jsonData.cacheLevel ?? PrometheusCacheLevel.Low;
this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters);
this.datasourceConfigurationPrometheusFlavor = instanceSettings.jsonData.prometheusType;
this.datasourceConfigurationPrometheusVersion = instanceSettings.jsonData.prometheusVersion;
this.disableRecordingRules = instanceSettings.jsonData.disableRecordingRules ?? false;
this.exemplarTraceIdDestinations = instanceSettings.jsonData.exemplarTraceIdDestinations;
this.exemplarsAvailable = true;
this.hasIncrementalQuery = instanceSettings.jsonData.incrementalQuerying ?? false;
this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET';
this.interval = instanceSettings.jsonData.timeInterval || '15s';
this.lookupsDisabled = instanceSettings.jsonData.disableMetricsLookup ?? false;
this.ruleMappings = {};
this.seriesEndpoint = instanceSettings.jsonData.seriesEndpoint ?? false;
this.seriesLimit = instanceSettings.jsonData.seriesLimit ?? DEFAULT_SERIES_LIMIT;
this.type = 'prometheus';
this.url = instanceSettings.url!;
this.withCredentials = Boolean(instanceSettings.withCredentials);
this.defaultEditor = instanceSettings.jsonData.defaultEditor;
// INHERITED PROPERTIES
this.annotations = PrometheusAnnotationSupport(this);
this.variables = new PrometheusVariableSupport(this, this.templateSrv);
// LANGUAGE PROVIDER
// This needs to be the last thing we initialize.
this.languageProvider = languageProvider ?? new PrometheusLanguageProvider(this);
}
/**
* Initializes the Prometheus datasource by loading recording rules and checking exemplar availability.
*
* This method performs two key initialization tasks: Loads recording rules from the
* Prometheus API and checks if exemplars are available by testing the exemplars API endpoint.
*/
init = async (): Promise<void> => {
if (!this.disableRecordingRules) {
this.loadRules();
}
this.exemplarsAvailable = await this.areExemplarsAvailable();
};
/**
* Loads recording rules from the Prometheus API and extracts rule mappings.
*
* This method fetches rules from the `/api/v1/rules` endpoint and processes
* them to create a mapping of rule names to their corresponding queries and labels.
* The rules API is experimental, so errors are logged but not thrown.
*/
private async loadRules(): Promise<void> {
try {
const params = {};
const options = { showErrorAlert: false };
const res = await this.metadataRequest('/api/v1/rules', params, options);
const ruleGroups = res.data?.data?.groups;
if (ruleGroups) {
this.ruleMappings = extractRuleMappingFromGroups(ruleGroups);
}
} catch (err) {
console.log('Rules API is experimental. Ignore next error.');
console.error(err);
}
}
/**
* Checks if exemplars are available by testing the exemplars API endpoint.
*
* This method makes a test request to the `/api/v1/query_exemplars` endpoint to determine
* if the Prometheus instance supports exemplars. The test uses a simple query with a
* 30-minute time range. If the request succeeds with a 'success' status, exemplars
* are considered available. Errors are caught and return false to avoid breaking
* the datasource initialization.
*/
private async areExemplarsAvailable(): Promise<boolean> {
try {
const params = {
query: 'test',
start: dateTime().subtract(30, 'minutes').valueOf().toString(),
end: dateTime().valueOf().toString(),
};
const options = { showErrorAlert: false };
const res = await this.metadataRequest('/api/v1/query_exemplars', params, options);
return res.data.status === 'success';
} catch (err) {
return false;
}
}
getQueryDisplayText(query: PromQuery) {
return query.expr;
}
/**
* Get target signature for query caching
* @param request
* @param query
*/
getPrometheusTargetSignature(request: DataQueryRequest<PromQuery>, query: PromQuery) {
const targExpr = this.interpolateString(query.expr);
return `${targExpr}|${query.interval ?? request.interval}|${JSON.stringify(request.rangeRaw ?? '')}|${
query.exemplar
}`;
}
hasLabelsMatchAPISupport(): boolean {
// users may choose the series endpoint as it has a POST method
// while the label values is only GET
if (this.seriesEndpoint) {
return false;
}
return (
// https://github.com/prometheus/prometheus/releases/tag/v2.24.0
this._isDatasourceVersionGreaterOrEqualTo('2.24.0', PromApplication.Prometheus) ||
// All versions of Mimir support matchers for labels API
this._isDatasourceVersionGreaterOrEqualTo('2.0.0', PromApplication.Mimir) ||
// https://github.com/cortexproject/cortex/discussions/4542
this._isDatasourceVersionGreaterOrEqualTo('1.11.0', PromApplication.Cortex) ||
// https://github.com/thanos-io/thanos/pull/3566
//https://github.com/thanos-io/thanos/releases/tag/v0.18.0
this._isDatasourceVersionGreaterOrEqualTo('0.18.0', PromApplication.Thanos)
);
}
_isDatasourceVersionGreaterOrEqualTo(targetVersion: string, targetFlavor: PromApplication): boolean {
// User hasn't configured flavor/version yet, default behavior is to support labels match api support
if (!this.datasourceConfigurationPrometheusVersion || !this.datasourceConfigurationPrometheusFlavor) {
return true;
}
if (targetFlavor !== this.datasourceConfigurationPrometheusFlavor) {
return false;
}
return gte(this.datasourceConfigurationPrometheusVersion, targetVersion);
}
_addTracingHeaders(httpOptions: PromQueryRequest, options: DataQueryRequest<PromQuery>) {
httpOptions.headers = {};
if (this.access === 'proxy') {
httpOptions.headers['X-Dashboard-UID'] = options.dashboardUID;
httpOptions.headers['X-Panel-Id'] = options.panelId;
}
}
directAccessError() {
return throwError(
() =>
new Error(
'Browser access mode in the Prometheus datasource is no longer available. Switch to server access mode.'
)
);
}
/**
* Any request done from this data source should go through here as it contains some common processing for the
* request. Any processing done here needs to be also copied on the backend as this goes through data source proxy
* but not through the same code as alerting.
*/
_request<T = unknown>(
url: string,
data: Record<string, string> | null,
overrides: Partial<BackendSrvRequest> = {}
): Observable<FetchResponse<T>> {
if (this.access === 'direct') {
return this.directAccessError();
}
data = data || {};
for (const [key, value] of this.customQueryParameters) {
if (data[key] == null) {
data[key] = value;
}
}
let queryUrl = this.url + url;
if (url.startsWith(`/api/datasources/uid/${this.uid}`)) {
// This url is meant to be a replacement for the whole URL. Replace the entire URL
queryUrl = url;
}
const options: BackendSrvRequest = defaults(overrides, {
url: queryUrl,
method: this.httpMethod,
headers: {},
});
if (options.method === 'GET') {
if (data && Object.keys(data).length) {
options.url =
options.url +
(options.url.search(/\?/) >= 0 ? '&' : '?') +
Object.entries(data)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
}
} else {
if (!options.headers!['Content-Type']) {
options.headers!['Content-Type'] = 'application/x-www-form-urlencoded';
}
options.data = data;
}
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
}
if (this.basicAuth) {
options.headers!.Authorization = this.basicAuth;
}
return getBackendSrv().fetch<T>(options);
}
async importFromAbstractQueries(abstractQueries: AbstractQuery[]): Promise<PromQuery[]> {
return abstractQueries.map((abstractQuery) => importFromAbstractQuery(abstractQuery));
}
async exportToAbstractQueries(queries: PromQuery[]): Promise<AbstractQuery[]> {
return queries.map((query) => exportToAbstractQuery(query));
}
// Use this for tab completion features, wont publish response to other components
async metadataRequest<T = any>(url: string, params = {}, options?: Partial<BackendSrvRequest>) {
// If URL includes endpoint that supports POST and GET method, try to use configured method. This might fail as POST is supported only in v2.10+.
if (GET_AND_POST_METADATA_ENDPOINTS.some((endpoint) => url.includes(endpoint))) {
try {
return await lastValueFrom(
this._request<T>(`/api/datasources/uid/${this.uid}/resources${url}`, params, {
method: this.httpMethod,
hideFromInspector: true,
showErrorAlert: false,
...options,
})
);
} catch (err) {
// If status code of error is Method Not Allowed (405) and HTTP method is POST, retry with GET
if (this.httpMethod === 'POST' && isFetchError(err) && (err.status === 405 || err.status === 400)) {
console.warn(`Couldn't use configured POST HTTP method for this request. Trying to use GET method instead.`);
} else {
throw err;
}
}
}
return await lastValueFrom(
this._request<T>(`/api/datasources/uid/${this.uid}/resources${url}`, params, {
method: 'GET',
hideFromInspector: true,
...options,
})
); // toPromise until we change getTagValues, getLabelNames to Observable
}
interpolateQueryExpr(value: string | string[] = [], variable: QueryVariableModel | CustomVariableModel) {
return interpolateQueryExpr(value, variable);
}
targetContainsTemplate(target: PromQuery) {
return this.templateSrv.containsTemplate(target.expr);
}
shouldRunExemplarQuery(target: PromQuery, request: DataQueryRequest<PromQuery>): boolean {
if (target.exemplar) {
// We check all already processed targets and only create exemplar target for not used metric names
const metricName = this.languageProvider.retrieveHistogramMetrics().find((m) => target.expr.includes(m));
// Remove targets that weren't processed yet (in targets array they are after current target)
const currentTargetIdx = request.targets.findIndex((t) => t.refId === target.refId);
const targets = request.targets.slice(0, currentTargetIdx).filter((t) => !t.hide);
if (!metricName || (metricName && !targets.some((t) => t.expr.includes(metricName)))) {
return true;
}
return false;
}
return false;
}
processTargetV2(target: PromQuery, request: DataQueryRequest<PromQuery>) {
// The `utcOffsetSec` parameter is required by the backend to correctly align time ranges.
// This alignment ensures that relative time ranges (e.g., "Last N hours/days/years") are adjusted
// according to the user's selected time zone, rather than defaulting to UTC.
//
// Example: If the user selects "Last 5 days," each day should begin at 00:00 in the chosen time zone,
// rather than at 00:00 UTC, ensuring an accurate breakdown.
//
// This adjustment does not apply to absolute time ranges, where users explicitly set
// the start and end timestamps.
//
// Handling `utcOffsetSec`:
// - When using the browser's time zone, the UTC offset is derived from the request range object.
// - When the user selects a custom time zone, the UTC offset must be calculated accordingly.
// More details:
// - Issue that led to the introduction of utcOffsetSec: https://github.com/grafana/grafana/issues/17278
// - Implementation PR: https://github.com/grafana/grafana/pull/17477
let utcOffset = request.range.to.utcOffset();
if (request.timezone === 'browser') {
// we need to check if the request is a relative or absolute range.
// if it is absolute time range then utcOffset must be 0. we don't care the offset
// because we are already sending the from and to values in utc. we don't need to adjust them again
// for relative ranges we need utcOffset to adjust query range.
utcOffset = this.isUsingRelativeTimeRange(request.range) ? utcOffset : 0;
} else {
utcOffset = tz(request.timezone).utcOffset();
}
const processedTargets: PromQuery[] = [];
const processedTarget = {
...target,
exemplar: this.shouldRunExemplarQuery(target, request),
requestId: request.panelId + target.refId,
utcOffsetSec: utcOffset * 60,
};
if (request.scopes) {
processedTarget.scopes = (request.scopes ?? []).map((scope) => ({
name: scope.metadata.name,
...scope.spec,
}));
}
if (config.featureToggles.groupByVariable || config.featureToggles.dashboardUnifiedDrilldownControls) {
processedTarget.groupByKeys = request.groupByKeys;
}
if (target.instant && target.range) {
// We have query type "Both" selected
// We should send separate queries with different refId
processedTargets.push(
{
...processedTarget,
refId: processedTarget.refId,
instant: false,
},
{
...processedTarget,
refId: processedTarget.refId + InstantQueryRefIdIndex,
range: false,
exemplar: false,
}
);
} else {
processedTargets.push(processedTarget);
}
return processedTargets;
}
query(request: DataQueryRequest<PromQuery>): Observable<DataQueryResponse> {
if (this.access === 'direct') {
return this.directAccessError();
}
// Use incremental query only if enabled and no instant queries or no $__range variables
const shouldUseIncrementalQuery =
this.hasIncrementalQuery &&
!config.publicDashboardAccessToken &&
!request.targets.some((target) => target.instant || target.expr?.includes('$__range'));
let fullOrPartialRequest: DataQueryRequest<PromQuery> = request;
let requestInfo: CacheRequestInfo<PromQuery> | undefined = undefined;
if (shouldUseIncrementalQuery) {
requestInfo = this.cache.requestInfo(request);
fullOrPartialRequest = requestInfo.requests[0];
}
const targets = fullOrPartialRequest.targets.map((target) => this.processTargetV2(target, fullOrPartialRequest));
const startTime = new Date();
return super.query({ ...fullOrPartialRequest, targets: targets.flat() }).pipe(
map((response) => {
const amendedResponse = {
...response,
data: this.cache.procFrames(request, requestInfo, response.data),
};
return transformV2(amendedResponse, request, {
exemplarTraceIdDestinations: this.exemplarTraceIdDestinations,
});
}),
tap((response: DataQueryResponse) => {
trackQuery(response, request, startTime);
})
);
}
metricFindQuery(query: string, options?: LegacyMetricFindQueryOptions) {
if (!query) {
return Promise.resolve([]);
}
const timeRange = options?.range ?? getDefaultTimeRange();
const scopedVars = {
...this.getIntervalVars(),
...this.getRangeScopedVars(timeRange),
};
const interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr);
const metricFindQuery = new PrometheusMetricFindQuery(this, interpolated);
return metricFindQuery.process(timeRange);
}
getIntervalVars() {
return {
__interval: { text: this.interval, value: this.interval },
__interval_ms: { text: rangeUtil.intervalToMs(this.interval), value: rangeUtil.intervalToMs(this.interval) },
};
}
getRangeScopedVars(range: TimeRange) {
const msRange = range.to.diff(range.from);
const sRange = Math.round(msRange / 1000);
return {
__range_ms: { text: msRange, value: msRange },
__range_s: { text: sRange, value: sRange },
__range: { text: sRange + 's', value: sRange + 's' },
};
}
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
// this is used to get label keys, a.k.a label names
// it is used in metric_find_query.ts
// and in Tempo here grafana/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx
async getTagKeys(options: DataSourceGetTagKeysOptions<PromQuery>): Promise<MetricFindValue[]> {
if (!options.timeRange) {
options.timeRange = getDefaultTimeRange();
}
if ((options?.scopes?.length ?? 0) > 0) {
const suggestions = await this.languageProvider.fetchSuggestions(
options.timeRange,
options.queries,
options.scopes,
options.filters
);
// filter out already used labels and empty labels
return suggestions
.filter((labelName) => !!labelName && !options.filters.find((filter) => filter.key === labelName))
.map((k) => ({ value: k, text: k }));
}
const match = extractResourceMatcher(options.queries ?? [], options.filters);
let labelKeys: string[] = await this.languageProvider.queryLabelKeys(options.timeRange, match);
// filter out already used labels
return labelKeys
.filter((labelName) => !options.filters.find((filter) => filter.key === labelName))
.map((k) => ({ value: k, text: k }));
}
// By implementing getGroupByKeys we add group by variable support independently from adhoc filters.
// It delegates to getTagKeys
async getGroupByKeys(options: DataSourceGetTagKeysOptions<PromQuery>): Promise<MetricFindValue[]> {
return this.getTagKeys(options);
}
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
async getTagValues(options: DataSourceGetTagValuesOptions<PromQuery>): Promise<MetricFindValue[]> {
if (!options.timeRange) {
options.timeRange = getDefaultTimeRange();
}
const requestId = `[${this.uid}][${options.key}]`;
if ((options?.scopes?.length ?? 0) > 0) {
return (
await this.languageProvider.fetchSuggestions(
options.timeRange,
options.queries,
options.scopes,
options.filters,
options.key,
undefined,
requestId
)
).map((v) => ({ value: v, text: v }));
}
const match = extractResourceMatcher(options.queries ?? [], options.filters);
return (await this.languageProvider.queryLabelValues(options.timeRange, options.key, match)).map((v) => ({
value: v,
text: v,
}));
}
interpolateVariablesInQueries(
queries: PromQuery[],
scopedVars: ScopedVars,
filters?: AdHocVariableFilter[]
): PromQuery[] {
let expandedQueries = queries;
if (queries && queries.length) {
expandedQueries = queries.map((query) => {
const interpolatedQuery = this.templateSrv.replace(
query.expr,
scopedVars,
this.interpolateExploreMetrics(query.fromExploreMetrics)
);
const replacedInterpolatedQuery = targetHasScopes(query)
? interpolatedQuery
: this.templateSrv.replace(
this.enhanceExprWithAdHocFilters(filters, interpolatedQuery),
scopedVars,
this.interpolateQueryExpr
);
const expandedQuery = {
...query,
...(query.scopes && query.scopes.length > 0 ? { adhocFilters: this.generateScopeFilters(filters) } : {}),
datasource: this.getRef(),
expr: replacedInterpolatedQuery,
interval: this.templateSrv.replace(query.interval, scopedVars),
};
return expandedQuery;
});
}
return expandedQueries;
}
getQueryHints(query: PromQuery, result: unknown[]) {
return getQueryHints(query.expr ?? '', result, this);
}
modifyQuery(query: PromQuery, action: QueryFixAction): PromQuery {
let expression = query.expr ?? '';
switch (action.type) {
case 'ADD_FILTER': {
const { key, value } = action.options ?? {};
if (key && value) {
expression = addLabelToQuery(expression, key, value);
}
break;
}
case 'ADD_FILTER_OUT': {
const { key, value } = action.options ?? {};
if (key && value) {
expression = addLabelToQuery(expression, key, value, '!=');
}
break;
}
case 'ADD_HISTOGRAM_QUANTILE': {
expression = `histogram_quantile(0.95, sum(rate(${expression}[$__rate_interval])) by (le))`;
break;
}
case 'ADD_HISTOGRAM_AVG': {
expression = `histogram_avg(rate(${expression}[$__rate_interval]))`;
break;
}
case 'ADD_HISTOGRAM_FRACTION': {
expression = `histogram_fraction(0,0.2,rate(${expression}[$__rate_interval]))`;
break;
}
case 'ADD_HISTOGRAM_COUNT': {
expression = `histogram_count(rate(${expression}[$__rate_interval]))`;
break;
}
case 'ADD_HISTOGRAM_SUM': {
expression = `histogram_sum(rate(${expression}[$__rate_interval]))`;
break;
}
case 'ADD_HISTOGRAM_STDDEV': {
expression = `histogram_stddev(rate(${expression}[$__rate_interval]))`;
break;
}
case 'ADD_HISTOGRAM_STDVAR': {
expression = `histogram_stdvar(rate(${expression}[$__rate_interval]))`;
break;
}
case 'ADD_RATE': {
expression = `rate(${expression}[$__rate_interval])`;
break;
}
case 'ADD_SUM': {
expression = `sum(${expression.trim()}) by ($1)`;
break;
}
case 'EXPAND_RULES': {
if (action.options) {
expression = expandRecordingRules(expression, action.options as any);
}
break;
}
default:
break;
}
return { ...query, expr: expression };
}
/**
* Returns the adjusted "snapped" interval parameters
*/
getAdjustedInterval(timeRange: TimeRange): { start: string; end: string } {
return getRangeSnapInterval(this.cacheLevel, timeRange);
}
/**
* This will return a time range that always includes the users current time range,
* and then a little extra padding to round up/down to the nearest nth minute,
* defined by the result of the getCacheDurationInMinutes.
*
* For longer cache durations, and shorter query durations,
* the window we're calculating might be much bigger then the user's current window,
* resulting in us returning labels/values that might not be applicable for the given window,
* this is a necessary trade-off if we want to cache larger durations
*/
getTimeRangeParams(timeRange: TimeRange): { start: string; end: string } {
return {
start: getPrometheusTime(timeRange.from, false).toString(),
end: getPrometheusTime(timeRange.to, true).toString(),
};
}
/**
* This converts the adhocVariableFilter array and converts it to scopeFilter array
* @param filters
*/
generateScopeFilters(filters?: AdHocVariableFilter[]): ScopeSpecFilter[] {
if (!filters) {
return [];
}
return filters.map((f) => ({
key: f.key,
operator: scopeFilterOperatorMap[f.operator],
value: this.templateSrv.replace(f.value, {}, this.interpolateQueryExpr),
values: f.values?.map((v) => this.templateSrv.replace(v, {}, this.interpolateQueryExpr)),
}));
}
enhanceExprWithAdHocFilters(filters: AdHocVariableFilter[] | undefined, expr: string) {
if (!filters || filters.length === 0) {
return expr;
}
const finalQuery = filters.map(remapOneOf).reduce((acc, filter) => {
const { key, operator } = filter;
let { value } = filter;
if (operator === '=~' || operator === '!~') {
value = prometheusRegularEscape(value);
}
return addLabelToQuery(acc, key, value, operator);
}, expr);
return finalQuery;
}
// Used when running queries through backend
filterQuery(query: PromQuery): boolean {
if (query.hide || !query.expr) {
return false;
}
return true;
}
// Used when running queries through backend
applyTemplateVariables(target: PromQuery, scopedVars: ScopedVars, filters?: AdHocVariableFilter[]) {
const variables = { ...scopedVars };
// We want to interpolate these variables on backend.
// The pre-calculated values are replaced withe the variable strings.
variables.__interval = {
value: '$__interval',
};
variables.__interval_ms = {
value: '$__interval_ms',
};
// interpolate expression
// We need a first replace to evaluate variables before applying adhoc filters
// This is required for an expression like `metric > $VAR` where $VAR is a float to which we must not add adhoc filters
const expr = this.templateSrv.replace(
target.expr,
variables,
this.interpolateExploreMetrics(target.fromExploreMetrics)
);
// Apply ad-hoc filters
// When ad-hoc filters are applied, we replace again the variables in case the ad-hoc filters also reference a variable
const exprWithAdhoc = targetHasScopes(target)
? expr
: this.templateSrv.replace(this.enhanceExprWithAdHocFilters(filters, expr), variables, this.interpolateQueryExpr);
return {
...target,
...(targetHasScopes(target) ? { adhocFilters: this.generateScopeFilters(filters) } : {}),
expr: exprWithAdhoc,
interval: this.templateSrv.replace(target.interval, variables),
legendFormat: this.templateSrv.replace(target.legendFormat, variables),
};
}
getVariables(): string[] {
return this.templateSrv.getVariables().map((v) => `$${v.name}`);
}
interpolateString(string: string, scopedVars?: ScopedVars) {
return this.templateSrv.replace(string, scopedVars, this.interpolateQueryExpr);
}
interpolateExploreMetrics(fromExploreMetrics?: boolean) {
return (value: string | string[] = [], variable: QueryVariableModel | CustomVariableModel) => {
if (typeof value === 'string' && fromExploreMetrics) {
if (variable.name === 'filters') {
return wrapUtf8Filters(value);
}
if (variable.name === 'groupby') {
return utf8Support(value);
}
}
return this.interpolateQueryExpr(value, variable);
};
}
isUsingRelativeTimeRange(range: TimeRange): boolean {
if (typeof range.raw.from !== 'string' || typeof range.raw.to !== 'string') {
return false;
}
return range.raw.from.includes('now') || range.raw.to.includes('now');
}
getDefaultQuery(app: CoreApp): PromQuery {
const defaults = {
refId: 'A',
expr: '',
range: true,
instant: false,
};
if (app === CoreApp.UnifiedAlerting) {
return {
...defaults,
instant: true,
range: false,
};
}
if (app === CoreApp.Explore) {
return {
...defaults,
instant: true,
range: true,
};
}
return defaults;
}
}
function targetHasScopes(target: PromQuery): boolean {
return !!(target.scopes && target.scopes.length > 0);
}
export function extractRuleMappingFromGroups(groups: RawRecordingRules[]): RuleQueryMapping {
return groups.reduce<RuleQueryMapping>(
(mapping, group) =>
group.rules
.filter((rule) => rule.type === 'recording')
.reduce((acc, rule) => {
// retrieve existing record
const existingRule = acc[rule.name] ?? [];
// push a new query with labels
existingRule.push({
query: rule.query,
labels: rule.labels,
});
acc[rule.name] = existingRule;
return acc;
}, mapping),
{}
);
}
/**
* It creates a matcher string for resource calls
* @param queries
* @param adhocFilters
*
* @example
* queries<PromQuery>=[{expr:`metricName{label="value"}`}]
* adhocFilters={key:"instance", operator:"=", value:"localhost"}
* returns {__name__=~"metricName", instance="localhost"}
*/
export const extractResourceMatcher = (
queries: PromQuery[],
adhocFilters: AdHocVariableFilter[]
): string | undefined => {
// Extract metric names from queries we have already
const metricMatch = populateMatchParamsFromQueries(queries);
const labelFilters: QueryBuilderLabelFilter[] = adhocFilters.map((f) => ({
label: f.key,
value: f.value,
op: f.operator,
}));
// Extract label filters from the filters we have already
const labelsMatch = renderLabelsWithoutBrackets(labelFilters);
if (metricMatch.length === 0 && labelsMatch.length === 0) {
return undefined;
}
// Create a matcher using metric names and label filters
return `{${[...metricMatch, ...labelsMatch].join(',')}}`;
};
export function remapOneOf(filter: AdHocVariableFilter) {
let { operator, value, values } = filter;
if (operator === '=|' || operator === '!=|') {
operator = operator === '=|' ? '=~' : '!~';
value = values?.map(prometheusRegularEscape).join('|') ?? '';
}
return {
...filter,
operator,
value,
};
}

View File

@@ -1,95 +0,0 @@
// NOTE: these two functions are similar to the escapeLabelValueIn* functions
// in language_utils.ts, but they are not exactly the same algorithm, and we found
import { type QueryVariableModel, type CustomVariableModel } from '@grafana/data';
import { config } from '@grafana/runtime';
export function interpolateQueryExpr(
value: string | string[] = [],
variable: QueryVariableModel | CustomVariableModel
) {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
return prometheusRegularEscape(value);
}
if (typeof value === 'string') {
return prometheusSpecialRegexEscape(value);
}
const escapedValues = value.map((val) => prometheusSpecialRegexEscape(val));
if (escapedValues.length === 1) {
return escapedValues[0];
}
return '(' + escapedValues.join('|') + ')';
}
// no way to reuse one in the another or vice versa.
export function prometheusRegularEscape<T>(value: T) {
if (typeof value !== 'string') {
return value;
}
if (config.featureToggles.prometheusSpecialCharsInLabelValues) {
// if the string looks like a complete label matcher (e.g. 'job="grafana"' or 'job=~"grafana"'),
// don't escape the encapsulating quotes
if (/^\w+(=|!=|=~|!~)".*"$/.test(value)) {
return value;
}
return value
.replace(/\\/g, '\\\\') // escape backslashes
.replace(/"/g, '\\"'); // escape double quotes
}
// classic behavior
return value
.replace(/\\/g, '\\\\') // escape backslashes
.replace(/'/g, "\\\\'"); // escape single quotes
}
export function prometheusSpecialRegexEscape<T>(value: T) {
if (typeof value !== 'string') {
return value;
}
if (config.featureToggles.prometheusSpecialCharsInLabelValues) {
return value
.replace(/\\/g, '\\\\\\\\') // escape backslashes
.replace(/"/g, '\\\\\\"') // escape double quotes
.replace(/[$^*{}\[\]\'+?.()|]/g, '\\\\$&'); // escape regex metacharacters
}
// classic behavior
return value
.replace(/\\/g, '\\\\\\\\') // escape backslashes
.replace(/[$^*{}\[\]+?.()|]/g, '\\\\$&'); // escape regex metacharacters
}
// NOTE: the following 2 exported functions are very similar to the prometheus*Escape
// functions in datasource.ts, but they are not exactly the same algorithm, and we found
// no way to reuse one in the another or vice versa.
// Prometheus regular-expressions use the RE2 syntax (https://github.com/google/re2/wiki/Syntax),
// so every character that matches something in that list has to be escaped.
// the list of metacharacters is: *+?()|\.[]{}^$
// we make a javascript regular expression that matches those characters:
const RE2_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g;
function escapePrometheusRegexp(value: string): string {
return value.replace(RE2_METACHARACTERS, '\\$&');
}
// based on the openmetrics-documentation, the 3 symbols we have to handle are:
// - \n ... the newline character
// - \ ... the backslash character
// - " ... the double-quote character
export function escapeLabelValueInExactSelector(labelValue: string): string {
return labelValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
}
export function escapeLabelValueInRegexSelector(labelValue: string): string {
return escapeLabelValueInExactSelector(escapePrometheusRegexp(labelValue));
}

View File

@@ -1 +0,0 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><g fill="#3b697e"><path d="m64.001 32c0-17.646-14.356-32.001-32.001-32.001-17.646 0-32.001 14.355-32.001 32.001s14.355 32.001 32.001 32.001 32.001-14.355 32.001-32.001zm-62.592 0c0-16.868 13.723-30.591 30.591-30.591s30.591 13.723 30.591 30.591-13.723 30.591-30.591 30.591-30.591-13.723-30.591-30.591z" fill-rule="nonzero"/><circle cx="49.3" cy="32.725" r="5.296"/><circle cx="31.875" cy="52.94" r="5.296"/><path d="m31.148 33.103 3.295-8.185 2.988 8.538h2.684c.226 2.71 1.641 5.186 3.861 6.757l-8.659 8.659.665.665 8.808-8.808c1.378.774 2.932 1.18 4.512 1.18 5.056 0 9.216-4.16 9.216-9.215 0-5.056-4.16-9.216-9.216-9.216-1.464 0-2.907.349-4.209 1.017l-9.039-8.587c.797-.953 1.234-2.156 1.234-3.399 0-2.906-2.391-5.297-5.297-5.297s-5.297 2.391-5.297 5.297 2.391 5.297 5.297 5.297c1.242 0 2.445-.436 3.397-1.233l8.86 8.417c-2.542 1.666-4.102 4.486-4.162 7.525h-1.987l-3.594-10.267-3.992 9.915h-4.457l-3.788 8.766-3.341-8.353-.01.004v-.09h-3.378c-.124-2.816-2.474-5.064-5.292-5.064-2.907 0-5.298 2.391-5.298 5.298 0 2.906 2.391 5.298 5.298 5.298 2.64 0 4.898-1.975 5.25-4.592h2.759l3.978 9.947 4.44-10.274zm18.152-8.685c4.54 0 8.276 3.736 8.276 8.276s-3.736 8.276-8.276 8.276-8.276-3.736-8.276-8.276c.005-4.538 3.737-8.271 8.276-8.276z" fill-rule="nonzero"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1 +0,0 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(0 14.3827 -14.3827 0 7.68261 2.43042)" gradientUnits="userSpaceOnUse" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#f2c144"/><stop offset=".24" stop-color="#f1a03b"/><stop offset=".57" stop-color="#f17a31"/><stop offset=".84" stop-color="#f0632a"/><stop offset="1" stop-color="#f05a28"/></linearGradient><path d="m1.941 13.102h2.194l1.538-2.953-1.065-2.043zm11.935-5.079-1.2 2.303 1.406 2.742 1.2-2.313zm-.364-.705-2.562-4.971-1.263 2.228 2.623 5.049zm-3.991 3.039 1.429 2.743h2.436l-2.648-5.083zm-5.276-2.952-1.195-2.305-1.525 2.933 1.172 2.269zm-3.077 1.327-1.09 2.152 1.2 2.179 1.063-2.057zm8.113-3.494-1.27 2.243 1.146 2.179 1.219-2.34zm-4.792-2.98-1.096 2.133 4.264 8.154 1.137-2.182-2.155-4.135z" fill="url(#a)" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 995 B

View File

@@ -1 +0,0 @@
<svg width="2490" height="2500" viewBox="0 0 256 257" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M128.001.667C57.311.667 0 57.971 0 128.664c0 70.69 57.311 127.998 128.001 127.998S256 199.354 256 128.664C256 57.97 198.689.667 128.001.667zm0 239.56c-20.112 0-36.419-13.435-36.419-30.004h72.838c0 16.566-16.306 30.004-36.419 30.004zm60.153-39.94H67.842V178.47h120.314v21.816h-.002zm-.432-33.045H68.185c-.398-.458-.804-.91-1.188-1.375-12.315-14.954-15.216-22.76-18.032-30.716-.048-.262 14.933 3.06 25.556 5.45 0 0 5.466 1.265 13.458 2.722-7.673-8.994-12.23-20.428-12.23-32.116 0-25.658 19.68-48.079 12.58-66.201 6.91.562 14.3 14.583 14.8 36.505 7.346-10.152 10.42-28.69 10.42-40.056 0-11.769 7.755-25.44 15.512-25.907-6.915 11.396 1.79 21.165 9.53 45.4 2.902 9.103 2.532 24.423 4.772 34.138.744-20.178 4.213-49.62 17.014-59.784-5.647 12.8.836 28.818 5.27 36.518 7.154 12.424 11.49 21.836 11.49 39.638 0 11.936-4.407 23.173-11.84 31.958 8.452-1.586 14.289-3.016 14.289-3.016l27.45-5.355c.002-.002-3.987 16.401-19.314 32.197z" fill="#DA4E31"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="2.41 2.16 355.42 355.17"><path fill="#fff" d="M9.49808 9.14822v341.59786h241.93485a99.66306 99.66306 0 0 0 99.657-99.663V9.14822zm266.263 215.32837h12.75033v12.75018h-12.75035zm-4.00963-54.89h20.71531v20.72131h-20.71533zm-3.186-54.10261h27.09335v27.11729h-27.09343zm-43.739 159.90321h12.7381v12.75018h-12.75017zm-3.98558-55.53932h20.72129V240.587h-20.72129zm-3.186-53.44731h27.12344v27.09337h-27.09339zm3.186-27.00322v-20.71528h20.72129v20.71527zm-97.82954 135.98981h12.75021v12.75018h-12.75021zm-3.98558-54.8901h20.72131v20.69121h-20.72124zm3.98558-34.1748v-12.75019h12.75021v12.75018zm-3.98558-67.64019h20.72131v20.71527h-20.72124zM72.10081 275.38715H84.875v12.75018H72.10081zm0-50.91056H84.875v12.75018H72.10081zm-3.97955-54.89h20.7153v20.72131h-20.7153zm-3.19206-54.10265h27.09939v27.11729H64.9292zM53.01459 52.67679h254.53469v50.91052H205.74622v203.6302h-50.90449v-203.6302H53.01459z"/></svg>

Before

Width:  |  Height:  |  Size: 958 B

View File

@@ -1,93 +0,0 @@
// The Grafana Prometheus library exports a number of components.
// There are main components that can be imported directly into your plugin module.ts file.
// There are also more granular components that can be used to build components, for example, the config section can be built with granular parts to allow for custom auths.
// COMPONENTS/
// Main export
export { PromQueryEditorByApp } from './components/PromQueryEditorByApp';
// The parts
export { MonacoQueryFieldLazy } from './components/monaco-query-field/MonacoQueryFieldLazy';
export { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
export { PromCheatSheet } from './components/PromCheatSheet';
export { MetricsBrowser } from './components/metrics-browser/MetricsBrowser';
export { PromExemplarField } from './components/PromExemplarField';
export { PromExploreExtraField } from './components/PromExploreExtraField';
export { PromQueryEditorForAlerting } from './components/PromQueryEditorForAlerting';
export { PromQueryField } from './components/PromQueryField';
export { PromVariableQueryEditor } from './components/VariableQueryEditor';
// CONFIGURATION/
// Main export
export { ConfigEditor } from './configuration/ConfigEditor';
export { overhaulStyles, validateInput, docsTip } from './configuration/shared/utils';
export { PROM_CONFIG_LABEL_WIDTH, InstantQueryRefIdIndex } from './constants';
// The parts
export { AlertingSettingsOverhaul } from './configuration/AlertingSettingsOverhaul';
export { DataSourceHttpSettingsOverhaul } from './configuration/DataSourceHttpSettingsOverhaul';
export { ExemplarSetting } from './configuration/ExemplarSetting';
export { ExemplarsSettings } from './configuration/ExemplarsSettings';
export { PromFlavorVersions } from './configuration/PromFlavorVersions';
export { PromSettings } from './configuration/PromSettings';
// QUERYBUILDER/
// The parts (The query builder is imported into PromQueryEditorByApp)
export { QueryPattern } from './querybuilder/QueryPattern';
export { QueryPatternsModal } from './querybuilder/QueryPatternsModal';
// QUERYBUILDER/COMPONENTS/
export { LabelFilterItem } from './querybuilder/components/LabelFilterItem';
export { LabelFilters } from './querybuilder/components/LabelFilters';
export { LabelParamEditor } from './querybuilder/components/LabelParamEditor';
export { MetricCombobox } from './querybuilder/components/MetricCombobox';
export { MetricsLabelsSection } from './querybuilder/components/MetricsLabelsSection';
export { NestedQuery } from './querybuilder/components/NestedQuery';
export { NestedQueryList } from './querybuilder/components/NestedQueryList';
export { PromQueryBuilder } from './querybuilder/components/PromQueryBuilder';
export { PromQueryBuilderContainer } from './querybuilder/components/PromQueryBuilderContainer';
export { PromQueryBuilderExplained } from './querybuilder/components/PromQueryBuilderExplained';
export { PromQueryBuilderOptions } from './querybuilder/components/PromQueryBuilderOptions';
export { PromQueryCodeEditor } from './querybuilder/components/PromQueryCodeEditor';
export { PromQueryEditorSelector } from './querybuilder/components/PromQueryEditorSelector';
export { PromQueryLegendEditor } from './querybuilder/components/PromQueryLegendEditor';
export { QueryPreview } from './querybuilder/components/QueryPreview';
export { MetricsModal } from './querybuilder/components/metrics-modal/MetricsModal';
// SRC/
// Main export
export { PrometheusDatasource } from './datasource';
// The parts
export { addLabelToQuery } from './add_label_to_query';
export { type QueryEditorMode, type PromQueryFormat, type Prometheus } from './dataquery';
export { interpolateQueryExpr } from './escaping';
export { loadResources } from './loadResources';
export { PrometheusMetricFindQuery } from './metric_find_query';
export { promqlGrammar } from './promql';
export { getQueryHints, getInitHints } from './query_hints';
export { transformV2, transformDFToTable, parseSampleValue, sortSeriesByLabel } from './result_transformer';
export {
type PromQuery,
PrometheusCacheLevel,
PromApplication,
type PromOptions,
type ExemplarTraceIdDestination,
type PromQueryRequest,
type PromMetricsMetadataItem,
type PromMetricsMetadata,
type PromValue,
type PromMetric,
type PromBuildInfoResponse,
LegendFormatMode,
PromVariableQueryType,
type PromVariableQuery,
type StandardPromVariableQuery,
} from './types';
export { PrometheusVariableSupport } from './variables';
export type { PrometheusLanguageProviderInterface } from './language_provider';
// For Metrics Drilldown
export { getPrometheusTime } from './language_utils';
export { isValidLegacyName, utf8Support, wrapUtf8Filters } from './utf8_support';
export { buildVisualQueryFromString } from './querybuilder/parsing';
export { PromQueryModeller } from './querybuilder/PromQueryModeller';
export { type QueryBuilderLabelFilter } from './querybuilder/shared/types';

View File

@@ -1,21 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.mock.ts
export class EmptyLanguageProviderMock {
metrics = [];
constructor() {}
start() {
return new Promise((resolve) => {
resolve('');
});
}
retrieveMetrics = jest.fn().mockReturnValue(['metric']);
queryLabelKeys = jest.fn().mockResolvedValue([]);
queryLabelValues = jest.fn().mockResolvedValue([]);
retrieveLabelKeys = jest.fn().mockReturnValue([]);
retrieveMetricsMetadata = jest
.fn()
.mockReturnValue({ histogram_metric_sum: { type: 'counter', help: '', unit: 'sum' } });
queryMetricsMetadata = jest.fn().mockResolvedValue({});
}

View File

@@ -1,549 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.test.ts
import { AbstractLabelOperator, dateTime, type TimeRange } from '@grafana/data';
import { getCacheDurationInMinutes } from './caching';
import { DEFAULT_SERIES_LIMIT } from './constants';
import { type PrometheusDatasource } from './datasource';
import {
exportToAbstractQuery,
importFromAbstractQuery,
populateMatchParamsFromQueries,
PrometheusLanguageProvider,
} from './language_provider';
import { getPrometheusTime, getRangeSnapInterval } from './language_utils';
import { PrometheusCacheLevel, type PromQuery } from './types';
jest.mock('./language_utils', () => ({
...jest.requireActual('./language_utils'),
getPrometheusTime: jest.requireActual('./language_utils').getPrometheusTime,
getRangeSnapInterval: jest.requireActual('./language_utils').getRangeSnapInterval,
}));
const now = new Date(1681300293392).getTime();
const timeRangeDurationSeconds = 1;
const toPrometheusTime = getPrometheusTime(dateTime(now), false);
const fromPrometheusTime = getPrometheusTime(dateTime(now - timeRangeDurationSeconds * 1000), false);
const toPrometheusTimeString = toPrometheusTime.toString(10);
const fromPrometheusTimeString = fromPrometheusTime.toString(10);
const getMockTimeRange = (): TimeRange => {
return {
to: dateTime(now).utc(),
from: dateTime(now).subtract(timeRangeDurationSeconds, 'second').utc(),
raw: {
from: fromPrometheusTimeString,
to: toPrometheusTimeString,
},
};
};
const getTimeRangeParams = (
timRange: TimeRange,
override?: Partial<{ start: string; end: string }>
): { start: string; end: string } => ({
start: fromPrometheusTimeString,
end: toPrometheusTimeString,
...override,
});
const getMockQuantizedTimeRangeParams = (override?: Partial<TimeRange>): TimeRange => ({
from: dateTime(fromPrometheusTime * 1000),
to: dateTime(toPrometheusTime * 1000),
raw: {
from: `now-${timeRangeDurationSeconds}s`,
to: 'now',
},
...override,
});
describe('PrometheusLanguageProvider', () => {
const defaultDatasource: PrometheusDatasource = {
metadataRequest: () => ({ data: { data: [] } }),
getTimeRangeParams: getTimeRangeParams,
interpolateString: (string: string) => string,
hasLabelsMatchAPISupport: () => false,
getDaysToCacheMetadata: () => 1,
getAdjustedInterval: () => getRangeSnapInterval(PrometheusCacheLevel.None, getMockQuantizedTimeRangeParams()),
cacheLevel: PrometheusCacheLevel.None,
getIntervalVars: () => ({}),
getRangeScopedVars: () => ({}),
seriesLimit: DEFAULT_SERIES_LIMIT,
} as unknown as PrometheusDatasource;
describe('constructor', () => {
it('should initialize with SeriesApiClient when labels match API is not supported', () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
expect(provider).toBeInstanceOf(PrometheusLanguageProvider);
});
it('should initialize with LabelsApiClient when labels match API is supported', () => {
const datasourceWithLabelsAPI = {
...defaultDatasource,
hasLabelsMatchAPISupport: () => true,
} as unknown as PrometheusDatasource;
const provider = new PrometheusLanguageProvider(datasourceWithLabelsAPI);
expect(provider).toBeInstanceOf(PrometheusLanguageProvider);
});
});
describe('start', () => {
it('should not start when lookups are disabled', async () => {
const datasourceWithLookupsDisabled = {
...defaultDatasource,
lookupsDisabled: true,
} as unknown as PrometheusDatasource;
const provider = new PrometheusLanguageProvider(datasourceWithLookupsDisabled);
const result = await provider.start();
expect(result).toEqual([]);
});
it('should use resource client and metricsMetadata is available', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const mockMetadata = { metric1: { type: 'counter', help: 'help text' } };
// Mock the resource client's start method
const resourceClientStartSpy = jest.spyOn(provider['resourceClient'], 'start');
const queryMetadataSpy = jest.spyOn(provider as any, '_queryMetadata').mockResolvedValue(mockMetadata);
await provider.start();
expect(resourceClientStartSpy).toHaveBeenCalled();
expect(queryMetadataSpy).toHaveBeenCalled();
expect(provider.retrieveMetricsMetadata()).toEqual(mockMetadata);
});
it('should call queryMetricsMetadata with datasource seriesLimit during start', async () => {
const customSeriesLimit = 5000;
const datasourceWithCustomLimit = {
...defaultDatasource,
seriesLimit: customSeriesLimit,
} as PrometheusDatasource;
const provider = new PrometheusLanguageProvider(datasourceWithCustomLimit);
const mockMetadata = { metric1: { type: 'counter', help: 'help text' } };
// Mock the resource client's start method
const resourceClientStartSpy = jest.spyOn(provider['resourceClient'], 'start').mockResolvedValue();
const queryMetricsMetadataSpy = jest.spyOn(provider, 'queryMetricsMetadata').mockResolvedValue(mockMetadata);
await provider.start();
expect(resourceClientStartSpy).toHaveBeenCalled();
expect(queryMetricsMetadataSpy).toHaveBeenCalledWith(customSeriesLimit);
});
});
describe('queryMetricsMetadata', () => {
it('should fetch and store metadata without limit', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const mockMetadata = { metric1: { type: 'counter', help: 'help text' } };
const queryMetadataSpy = jest.spyOn(provider as any, '_queryMetadata').mockResolvedValue(mockMetadata);
const result = await provider.queryMetricsMetadata();
expect(queryMetadataSpy).toHaveBeenCalledWith(undefined);
expect(result).toEqual(mockMetadata);
expect(provider.retrieveMetricsMetadata()).toEqual(mockMetadata);
});
it('should fetch and store metadata with custom limit', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const mockMetadata = { metric1: { type: 'counter', help: 'help text' } };
const customLimit = 1000;
const queryMetadataSpy = jest.spyOn(provider as any, '_queryMetadata').mockResolvedValue(mockMetadata);
// TODO spy on /api/v1/metadata endpoint or fetch
const result = await provider.queryMetricsMetadata(customLimit);
expect(queryMetadataSpy).toHaveBeenCalledWith(customLimit);
expect(result).toEqual(mockMetadata);
expect(provider.retrieveMetricsMetadata()).toEqual(mockMetadata);
});
it('should pass limit parameter to the metadata API endpoint', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const requestSpy = jest.spyOn(provider, 'request').mockResolvedValue({
metric1: { type: 'counter', help: 'help text' },
});
const customLimit = 500;
await provider.queryMetricsMetadata(customLimit);
expect(requestSpy).toHaveBeenCalledWith(
'/api/v1/metadata',
{ limit: customLimit },
expect.objectContaining({
showErrorAlert: false,
})
);
});
it('should use DEFAULT_SERIES_LIMIT when no limit is provided', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const requestSpy = jest.spyOn(provider, 'request').mockResolvedValue({
metric1: { type: 'counter', help: 'help text' },
});
await provider.queryMetricsMetadata();
expect(requestSpy).toHaveBeenCalledWith(
'/api/v1/metadata',
{ limit: DEFAULT_SERIES_LIMIT },
expect.objectContaining({
showErrorAlert: false,
})
);
});
it('should pass zero limit when explicitly set', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const requestSpy = jest.spyOn(provider, 'request').mockResolvedValue({
metric1: { type: 'counter', help: 'help text' },
});
await provider.queryMetricsMetadata(0);
expect(requestSpy).toHaveBeenCalledWith(
'/api/v1/metadata',
{ limit: 0 },
expect.objectContaining({
showErrorAlert: false,
})
);
});
it('should include cache headers in the request', async () => {
const provider = new PrometheusLanguageProvider({
...defaultDatasource,
cacheLevel: PrometheusCacheLevel.Medium,
} as PrometheusDatasource);
const requestSpy = jest.spyOn(provider, 'request').mockResolvedValue({});
await provider.queryMetricsMetadata(1000);
expect(requestSpy).toHaveBeenCalledWith(
'/api/v1/metadata',
{ limit: 1000 },
expect.objectContaining({
showErrorAlert: false,
headers: expect.objectContaining({
'X-Grafana-Cache': expect.stringMatching(/private, max-age=\d+/),
}),
})
);
});
it('should handle undefined metadata response', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const queryMetadataSpy = jest.spyOn(provider as any, '_queryMetadata').mockResolvedValue(undefined);
const result = await provider.queryMetricsMetadata();
expect(queryMetadataSpy).toHaveBeenCalled();
expect(result).toEqual({});
expect(provider.retrieveMetricsMetadata()).toEqual({});
});
it('should handle null metadata response', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const queryMetadataSpy = jest.spyOn(provider as any, '_queryMetadata').mockResolvedValue(null);
const result = await provider.queryMetricsMetadata(1000);
expect(queryMetadataSpy).toHaveBeenCalledWith(1000);
expect(result).toEqual({});
expect(provider.retrieveMetricsMetadata()).toEqual({});
});
it('should handle endpoint errors and set empty metadata', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const queryMetadataSpy = jest
.spyOn(provider as any, '_queryMetadata')
.mockRejectedValue(new Error('Endpoint not found'));
const result = await provider.queryMetricsMetadata(1000);
expect(queryMetadataSpy).toHaveBeenCalledWith(1000);
expect(result).toEqual({});
expect(provider.retrieveMetricsMetadata()).toEqual({});
});
it('should handle network timeout errors gracefully', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const timeoutError = new Error('Request timeout');
timeoutError.name = 'TimeoutError';
const queryMetadataSpy = jest.spyOn(provider as any, '_queryMetadata').mockRejectedValue(timeoutError);
const result = await provider.queryMetricsMetadata(500);
expect(queryMetadataSpy).toHaveBeenCalledWith(500);
expect(result).toEqual({});
expect(provider.retrieveMetricsMetadata()).toEqual({});
});
it('should maintain backward compatibility by setting deprecated metricsMetadata property', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const mockMetadata = { metric1: { type: 'counter', help: 'help text' } };
jest.spyOn(provider as any, '_queryMetadata').mockResolvedValue(mockMetadata);
await provider.queryMetricsMetadata(250);
expect(provider.retrieveMetricsMetadata()).toEqual(mockMetadata);
});
});
describe('queryLabelKeys and queryLabelValues', () => {
const timeRange = getMockTimeRange();
it('should delegate to resource client queryLabelKeys', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const resourceClientSpy = jest
.spyOn(provider['resourceClient'], 'queryLabelKeys')
.mockResolvedValue(['label1', 'label2']);
const result = await provider.queryLabelKeys(timeRange, '{job="grafana"}');
expect(resourceClientSpy).toHaveBeenCalledWith(timeRange, '{job="grafana"}', undefined);
expect(result).toEqual(['label1', 'label2']);
});
it('queryLabelKeys should interpolate variables', async () => {
const provider = new PrometheusLanguageProvider({
...defaultDatasource,
interpolateString: (string: string) => string.replace(/\$/g, 'interpolated_'),
} as PrometheusDatasource);
const resourceClientSpy = jest
.spyOn(provider['resourceClient'], 'queryLabelKeys')
.mockResolvedValue(['label1', 'label2']);
const result = await provider.queryLabelKeys(timeRange, '{job="$job"}');
expect(resourceClientSpy).toHaveBeenCalledWith(timeRange, '{job="interpolated_job"}', undefined);
expect(result).toEqual(['label1', 'label2']);
});
it('should delegate to resource client queryLabelValues', async () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const resourceClientSpy = jest
.spyOn(provider['resourceClient'], 'queryLabelValues')
.mockResolvedValue(['value1', 'value2']);
const result = await provider.queryLabelValues(timeRange, 'job', '{job="grafana"}');
expect(resourceClientSpy).toHaveBeenCalledWith(timeRange, 'job', '{job="grafana"}', undefined);
expect(result).toEqual(['value1', 'value2']);
});
it('queryLabelValues should interpolate variables', async () => {
const provider = new PrometheusLanguageProvider({
...defaultDatasource,
interpolateString: (string: string) => string.replace(/\$/g, 'interpolated_'),
} as PrometheusDatasource);
const resourceClientSpy = jest
.spyOn(provider['resourceClient'], 'queryLabelValues')
.mockResolvedValue(['label1', 'label2']);
const result = await provider.queryLabelValues(timeRange, '$label', '{job="$job"}');
expect(resourceClientSpy).toHaveBeenCalledWith(
timeRange,
'interpolated_label',
'{job="interpolated_job"}',
undefined
);
expect(result).toEqual(['label1', 'label2']);
});
});
describe('retrieveMethods', () => {
it('should delegate to resource client for metrics and labels', () => {
const provider = new PrometheusLanguageProvider(defaultDatasource);
const mockResourceClient = {
histogramMetrics: ['histogram1', 'histogram2'],
metrics: ['metric1', 'metric2'],
labelKeys: ['label1', 'label2'],
};
// Mock the resource client properties
Object.defineProperty(provider, '_resourceClient', {
value: mockResourceClient,
writable: true,
});
expect(provider.retrieveHistogramMetrics()).toEqual(['histogram1', 'histogram2']);
expect(provider.retrieveMetrics()).toEqual(['metric1', 'metric2']);
expect(provider.retrieveLabelKeys()).toEqual(['label1', 'label2']);
});
});
describe('populateMatchParamsFromQueries', () => {
it('should add match params from queries', () => {
const queries: PromQuery[] = [
{ expr: 'metric1', refId: '1' },
{ expr: 'metric2', refId: '2' },
];
const result = populateMatchParamsFromQueries(queries);
expect(result).toEqual([`__name__=~"metric1|metric2"`]);
});
it('should handle binary queries', () => {
const queries: PromQuery[] = [{ expr: 'binary{label="val"} + second{}', refId: '1' }];
const result = populateMatchParamsFromQueries(queries);
expect(result).toEqual([`__name__=~"binary|second"`]);
});
it('should handle undefined queries', () => {
const result = populateMatchParamsFromQueries(undefined);
expect(result).toEqual([]);
});
it('should handle UTF8 metrics', () => {
const queries: PromQuery[] = [{ expr: '{"utf8.metric", label="value"}', refId: '1' }];
const result = populateMatchParamsFromQueries(queries);
expect(result).toContain('__name__=~"utf8.metric"');
});
it('should handle UTF8 metrics with normal metrics', () => {
const queries: PromQuery[] = [{ expr: '{"utf8.metric", label="value"} + second{}', refId: '1' }];
const result = populateMatchParamsFromQueries(queries);
expect(result).toContain('__name__=~"utf8.metric|second"');
});
it('should return match-all matcher if there is no expr in queries', () => {
const queries: PromQuery[] = [{ expr: '', refId: '1' }];
const result = populateMatchParamsFromQueries(queries);
expect(result).toEqual([]);
});
it('should return match-all matcher if there is no query', () => {
const queries: PromQuery[] = [];
const result = populateMatchParamsFromQueries(queries);
expect(result).toEqual([]);
});
it('should extract the correct matcher for queries with `... or vector(0)`', () => {
const queries: PromQuery[] = [
{
refId: 'A',
expr: `sum(increase(go_cpu_classes_idle_cpu_seconds_total[$__rate_interval])) or vector(0)`,
},
];
const result = populateMatchParamsFromQueries(queries);
expect(result).toEqual(['__name__=~"go_cpu_classes_idle_cpu_seconds_total"']);
});
});
describe('fetchSuggestions', () => {
it('should send POST request with correct parameters', async () => {
const timeRange = getMockTimeRange();
const mockQueries: PromQuery[] = [{ refId: 'A', expr: 'metric1' }];
const languageProvider = new PrometheusLanguageProvider({
...defaultDatasource,
interpolateString: (string: string) => `interpolated_${string}`,
getIntervalVars: () => ({ __interval: '1m' }),
getRangeScopedVars: () => ({ __range: { text: '1h', value: '1h' } }),
} as unknown as PrometheusDatasource);
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['suggestion1', 'suggestion2']);
// Simplifying the test by not passing complex scope objects that require more type definitions
const result = await languageProvider.fetchSuggestions(
timeRange,
mockQueries,
undefined, // omitting scopes parameter
[{ key: 'instance', operator: '=', value: 'localhost' }],
'metric',
100
);
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy.mock.calls[0][0]).toBe('/suggestions');
// Check method and content type
expect(requestSpy.mock.calls[0][2]).toMatchObject({
headers: { 'Content-Type': 'application/json' },
method: 'POST',
});
// Check query parameters
expect(requestSpy.mock.calls[0][1]).toMatchObject({
labelName: 'metric',
limit: 100,
queries: ['interpolated_metric1'],
});
expect(result).toEqual(['suggestion1', 'suggestion2']);
});
it('should use default time range if not provided', async () => {
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']);
await languageProvider.fetchSuggestions(undefined, [], [], [], 'test');
expect(requestSpy).toHaveBeenCalled();
// Default time range should be used
expect(requestSpy.mock.calls[0][1]).toHaveProperty('start');
expect(requestSpy.mock.calls[0][1]).toHaveProperty('end');
});
it('should handle empty response gracefully', async () => {
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
jest.spyOn(languageProvider, 'request').mockResolvedValue(null);
const result = await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test');
expect(result).toEqual([]);
});
it('should include cache headers when cacheLevel is set', async () => {
const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Medium);
const languageProvider = new PrometheusLanguageProvider({
...defaultDatasource,
cacheLevel: PrometheusCacheLevel.Medium,
} as PrometheusDatasource);
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']);
await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test');
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy.mock.calls[0][2]?.headers).toHaveProperty('X-Grafana-Cache');
expect(requestSpy.mock.calls[0][2]?.headers?.['X-Grafana-Cache']).toContain(
`private, max-age=${timeSnapMinutes * 60}`
);
});
});
});
describe('Query transformation', () => {
describe('importFromAbstractQuery', () => {
it('should handle empty queries', async () => {
const result = importFromAbstractQuery({ refId: 'bar', labelMatchers: [] });
expect(result).toEqual({ refId: 'bar', expr: '', range: true });
});
});
describe('exportToAbstractQuery', () => {
it('should extract labels and metric name from PromQL', async () => {
const abstractQuery = exportToAbstractQuery({
refId: 'bar',
expr: 'metric_name{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}',
instant: true,
range: false,
});
expect(abstractQuery).toMatchObject({
refId: 'bar',
labelMatchers: [
{ name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' },
{ name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' },
{ name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' },
{ name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' },
{ name: '__name__', operator: AbstractLabelOperator.Equal, value: 'metric_name' },
],
});
});
});
});

View File

@@ -1,448 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.ts
import Prism from 'prismjs';
import {
type AbstractLabelMatcher,
AbstractLabelOperator,
type AbstractQuery,
type AdHocVariableFilter,
getDefaultTimeRange,
type Scope,
scopeFilterOperatorMap,
type ScopeSpecFilter,
type TimeRange,
} from '@grafana/data';
import { type BackendSrvRequest } from '@grafana/runtime';
import { buildCacheHeaders, getDaysToCacheMetadata, getDefaultCacheHeaders } from './caching';
import { type PrometheusDatasource } from './datasource';
import { extractLabelMatchers, fixSummariesMetadata, toPromLikeQuery } from './language_utils';
import { promqlGrammar } from './promql';
import { buildVisualQueryFromString } from './querybuilder/parsing';
import { LabelsApiClient, type ResourceApiClient, SeriesApiClient } from './resource_clients';
import { type PromMetricsMetadata, type PromQuery } from './types';
interface PrometheusBaseLanguageProvider {
datasource: PrometheusDatasource;
/**
* When no timeRange provided, we will use the default time range (now/now-6h)
* @param timeRange
*/
start: (timeRange?: TimeRange) => Promise<unknown[]>;
request: (url: string, params?: any, options?: Partial<BackendSrvRequest>) => Promise<any>;
fetchSuggestions: (
timeRange?: TimeRange,
queries?: PromQuery[],
scopes?: Scope[],
adhocFilters?: AdHocVariableFilter[],
labelName?: string,
limit?: number,
requestId?: string
) => Promise<string[]>;
}
/**
* Modern implementation of the Prometheus language provider that abstracts API endpoint selection.
*
* Features:
* - Automatically selects the most efficient API endpoint based on Prometheus version and configuration
* - Supports both labels and series endpoints for backward compatibility
* - Handles match[] parameters for filtering time series data
* - Implements automatic request limiting (default: 40,000 series if not configured otherwise)
* - Provides unified interface for both modern and legacy Prometheus versions
* - Provides caching mechanism based on time range, limit, and match parameters
*
* @see LabelsApiClient For modern Prometheus versions using the labels API
* @see SeriesApiClient For legacy Prometheus versions using the series API
*/
export interface PrometheusLanguageProviderInterface extends PrometheusBaseLanguageProvider {
/**
* Initializes the language provider by fetching metrics, label keys, and metrics metadata using Resource Clients.
* All calls use the limit parameter from datasource configuration (default: 40,000 if not set).
*
* For backward compatibility, it calls _backwardCompatibleStart.
* Some places still rely on deprecated fields. Until we replace them, we need _backwardCompatibleStart method.
*/
start: (timeRange?: TimeRange) => Promise<unknown[]>;
/**
* Returns already cached metrics metadata including type and help information.
* If there is no cached metadata, it returns an empty object.
* To get fresh metadata, use queryMetricsMetadata instead.
*/
retrieveMetricsMetadata: () => PromMetricsMetadata;
/**
* Returns already cached list of histogram metrics (identified by '_bucket' suffix).
* If there are no cached histogram metrics, it returns an empty array.
*/
retrieveHistogramMetrics: () => string[];
/**
* Returns already cached list of all available metric names.
* If there are no cached metrics, it returns an empty array.
*/
retrieveMetrics: () => string[];
/**
* Returns already cached list of available label keys.
* If there are no cached label keys, it returns an empty array.
*/
retrieveLabelKeys: () => string[];
/**
* Fetches fresh metrics metadata from Prometheus with optional limit.
* Uses datasource's default limit if not specified.
*/
queryMetricsMetadata: (limit?: number) => Promise<PromMetricsMetadata>;
/**
* Queries Prometheus for label keys within time range, optionally filtered by match selector.
* Automatically selects labels or series endpoint based on datasource configuration.
* If no limit is provided, uses the datasource's default limit configuration.
* Use zero (0) to fetch all label keys, but this might return huge amounts of data.
*/
queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: number) => Promise<string[]>;
/**
* Queries Prometheus for values of a specific label key, optionally filtered by match selector.
* Automatically selects labels or series endpoint based on datasource configuration.
* If no limit is provided, uses the datasource's default limit configuration.
* Use zero (0) to fetch all label values, but this might return huge amounts of data.
*/
queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: number) => Promise<string[]>;
}
export class PrometheusLanguageProvider implements PrometheusLanguageProviderInterface {
public datasource: PrometheusDatasource;
private _metricsMetadata?: PromMetricsMetadata;
private _resourceClient?: ResourceApiClient;
constructor(datasource: PrometheusDatasource) {
this.datasource = datasource;
}
request = async (url: string, params = {}, options?: Partial<BackendSrvRequest>) => {
try {
const res = await this.datasource.metadataRequest(url, params, options);
return res.data.data;
} catch (error) {
if (!isCancelledError(error)) {
console.error(error);
}
}
return undefined;
};
/**
* Lazily initializes and returns the appropriate resource client based on Prometheus version.
*
* The client selection logic:
* - For Prometheus v2.6+ with labels API: Uses LabelsApiClient for efficient label-based queries
* - For older versions: Falls back to SeriesApiClient for backward compatibility
*
* The client instance is cached after first initialization to avoid repeated creation.
*
* @returns {ResourceApiClient} An instance of either LabelsApiClient or SeriesApiClient
*/
private get resourceClient(): ResourceApiClient {
if (!this._resourceClient) {
this._resourceClient = this.datasource.hasLabelsMatchAPISupport()
? new LabelsApiClient(this.request, this.datasource)
: new SeriesApiClient(this.request, this.datasource);
}
return this._resourceClient;
}
/**
* Same start logic but it uses resource clients. Backward compatibility it calls _backwardCompatibleStart.
* Some places still relies on deprecated fields. Until we replace them we need _backwardCompatibleStart method
*/
start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise<unknown[]> => {
if (this.datasource.lookupsDisabled) {
return [];
}
return await Promise.all([
this.resourceClient.start(timeRange),
this.queryMetricsMetadata(this.datasource.seriesLimit),
]);
};
/**
* Fetches metadata for metrics from Prometheus.
* Sets cache headers based on the configured metadata cache duration.
*
* @returns {Promise<PromMetricsMetadata>} Promise that resolves when metadata has been fetched
*/
private _queryMetadata = async (limit?: number): Promise<PromMetricsMetadata> => {
const secondsInDay = 86400;
const headers = buildCacheHeaders(getDaysToCacheMetadata(this.datasource.cacheLevel) * secondsInDay);
const metadata = await this.request(
`/api/v1/metadata`,
{ limit: limit ?? this.datasource.seriesLimit },
{
showErrorAlert: false,
...headers,
}
);
return fixSummariesMetadata(metadata);
};
/**
* Retrieves the cached Prometheus metrics metadata.
* This metadata includes type information (counter, gauge, etc.) and help text for metrics.
*
* @returns {PromMetricsMetadata} Cached metadata or empty object if not yet fetched
*/
public retrieveMetricsMetadata = (): PromMetricsMetadata => {
return this._metricsMetadata ?? {};
};
/**
* Retrieves the list of histogram metrics from the current resource client.
* Histogram metrics are identified by the '_bucket' suffix and are used for percentile calculations.
*
* @returns {string[]} Array of histogram metric names
*/
public retrieveHistogramMetrics = (): string[] => {
return this.resourceClient?.histogramMetrics;
};
/**
* Retrieves the complete list of available metrics from the current resource client.
* This includes all metric names regardless of their type (counter, gauge, histogram).
*
* @returns {string[]} Array of all metric names
*/
public retrieveMetrics = (): string[] => {
return this.resourceClient?.metrics;
};
/**
* Retrieves the list of available label keys from the current resource client.
* Label keys are the names of labels that can be used to filter and group metrics.
*
* @returns {string[]} Array of label key names
*/
public retrieveLabelKeys = (): string[] => {
return this.resourceClient?.labelKeys;
};
/**
* Fetches fresh metrics metadata from Prometheus and updates the cache.
* This includes querying for metric types, help text, and unit information.
* If the fetch fails, the cache is set to an empty object to prevent stale data.
*
* @returns {Promise<PromMetricsMetadata>} Promise that resolves to the fetched metadata
*/
public queryMetricsMetadata = async (limit?: number): Promise<PromMetricsMetadata> => {
try {
this._metricsMetadata = (await this._queryMetadata(limit)) ?? {};
} catch (error) {
this._metricsMetadata = {};
}
return this._metricsMetadata;
};
/**
* Fetches all available label keys that match the specified criteria.
*
* This method queries Prometheus for label keys within the specified time range.
* The results can be filtered using the match parameter and limited in size.
* Uses either the labels API (Prometheus v2.6+) or series API based on version.
*
* @param {TimeRange} timeRange - Time range to search for label keys
* @param {string} [match] - Optional PromQL selector to filter label keys (e.g., '{job="grafana"}')
* @param {string} [limit] - Optional maximum number of label keys to return
* @returns {Promise<string[]>} Array of matching label key names, sorted alphabetically
*/
public queryLabelKeys = async (timeRange: TimeRange, match?: string, limit?: number): Promise<string[]> => {
const interpolatedMatch = match ? this.datasource.interpolateString(match) : match;
return await this.resourceClient.queryLabelKeys(timeRange, interpolatedMatch, limit);
};
/**
* Fetches all values for a specific label key that match the specified criteria.
*
* This method queries Prometheus for label values within the specified time range.
* Results can be filtered using the match parameter to find values in specific contexts.
* Supports both modern (labels API) and legacy (series API) Prometheus versions.
*
* The method automatically handles UTF-8 encoded label keys by properly escaping them
* before making API requests. This means you can safely pass label keys containing
* special characters like dots, colons, or Unicode characters (e.g., 'http.status:code',
* 'μs', 'response.time').
*
* @param {TimeRange} timeRange - Time range to search for label values
* @param {string} labelKey - The label key to fetch values for (e.g., 'job', 'instance', 'http.status:code')
* @param {string} [match] - Optional PromQL selector to filter values (e.g., '{job="grafana"}')
* @param {string} [limit] - Optional maximum number of values to return
* @returns {Promise<string[]>} Array of matching label values, sorted alphabetically
* @example
* // Fetch all values for the 'job' label
* const values = await queryLabelValues(timeRange, 'job');
* // Fetch 'instance' values only for jobs matching 'grafana'
* const instances = await queryLabelValues(timeRange, 'instance', '{job="grafana"}');
* // Fetch values for a label key with special characters
* const statusCodes = await queryLabelValues(timeRange, 'http.status:code');
*/
public queryLabelValues = async (
timeRange: TimeRange,
labelKey: string,
match?: string,
limit?: number
): Promise<string[]> => {
const interpolatedMatch = match ? this.datasource.interpolateString(match) : match;
return await this.resourceClient.queryLabelValues(
timeRange,
this.datasource.interpolateString(labelKey),
interpolatedMatch,
limit
);
};
/**
* Fetch labels or values for a label based on the queries, scopes, filters and time range
*/
fetchSuggestions = async (
timeRange?: TimeRange,
queries?: PromQuery[],
scopes?: Scope[],
adhocFilters?: AdHocVariableFilter[],
labelName?: string,
limit?: number,
requestId?: string
): Promise<string[]> => {
if (!timeRange) {
timeRange = getDefaultTimeRange();
}
const url = '/suggestions';
const timeParams = this.datasource.getAdjustedInterval(timeRange);
const value = await this.request(
url,
{
labelName,
queries: queries?.map((q) =>
this.datasource.interpolateString(q.expr, {
...this.datasource.getIntervalVars(),
...this.datasource.getRangeScopedVars(timeRange),
})
),
scopes: scopes?.reduce<ScopeSpecFilter[]>((acc, scope) => {
if (scope.spec.filters) {
acc.push(...scope.spec.filters);
}
return acc;
}, []),
adhocFilters: adhocFilters?.map((filter) => ({
key: filter.key,
operator: scopeFilterOperatorMap[filter.operator],
value: filter.value,
values: filter.values,
})),
limit,
...timeParams,
},
{
...(requestId && { requestId }),
headers: {
...getDefaultCacheHeaders(this.datasource.cacheLevel)?.headers,
'Content-Type': 'application/json',
},
method: 'POST',
}
);
return value ?? [];
};
}
export const importFromAbstractQuery = (labelBasedQuery: AbstractQuery): PromQuery => {
return toPromLikeQuery(labelBasedQuery);
};
export const exportToAbstractQuery = (query: PromQuery): AbstractQuery => {
const promQuery = query.expr;
if (!promQuery || promQuery.length === 0) {
return { refId: query.refId, labelMatchers: [] };
}
const tokens = Prism.tokenize(promQuery, promqlGrammar);
const labelMatchers: AbstractLabelMatcher[] = extractLabelMatchers(tokens);
const nameLabelValue = getNameLabelValue(promQuery, tokens);
if (nameLabelValue && nameLabelValue.length > 0) {
labelMatchers.push({
name: '__name__',
operator: AbstractLabelOperator.Equal,
value: nameLabelValue,
});
}
return {
refId: query.refId,
labelMatchers,
};
};
/**
* Checks if an error is a cancelled request error.
* Used to avoid logging cancelled request errors.
*
* @param {unknown} error - Error to check
* @returns {boolean} True if the error is a cancelled request error
*/
function isCancelledError(error: unknown): error is {
cancelled: boolean;
} {
return typeof error === 'object' && error !== null && 'cancelled' in error && error.cancelled === true;
}
function getNameLabelValue(promQuery: string, tokens: Array<string | Prism.Token>): string {
let nameLabelValue = '';
for (const token of tokens) {
if (typeof token === 'string') {
nameLabelValue = token;
break;
}
}
return nameLabelValue;
}
/**
* Extracts metrics from queries and populates match parameters.
* This is used to filter time series data based on existing queries.
* Handles UTF8 metrics by properly escaping them.
*
* @param {PromQuery[]} queries - Array of Prometheus queries
* @returns {string[]} Metric names as a regex matcher inside the array for easy handling
*/
export const populateMatchParamsFromQueries = (queries?: PromQuery[]): string[] => {
if (!queries) {
return [];
}
const metrics = (queries ?? []).reduce<string[]>((params, query) => {
const visualQuery = buildVisualQueryFromString(query.expr);
if (visualQuery.query.metric !== '') {
params.push(visualQuery.query.metric);
}
if (visualQuery.query.binaryQueries) {
visualQuery.query.binaryQueries.forEach((bq) => {
if (bq.query.metric !== '') {
params.push(bq.query.metric);
}
});
}
return params;
}, []);
return metrics.length === 0 ? [] : [`__name__=~"${metrics.join('|')}"`];
};

View File

@@ -1,563 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_utils.test.ts
import { type Moment } from 'moment';
import { AbstractLabelOperator, type AbstractQuery, type DateTime, dateTime, type TimeRange } from '@grafana/data';
import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from './escaping';
import {
expandRecordingRules,
fixSummariesMetadata,
getPrometheusTime,
getRangeSnapInterval,
removeQuotesIfExist,
toPromLikeQuery,
truncateResult,
} from './language_utils';
import { PrometheusCacheLevel } from './types';
describe('fixSummariesMetadata', () => {
const synthetics = {
ALERTS: {
type: 'gauge',
help: 'Time series showing pending and firing alerts. The sample value is set to 1 as long as the alert is in the indicated active (pending or firing) state.',
},
};
it('returns only synthetics on empty metadata', () => {
expect(fixSummariesMetadata({})).toEqual({ ...synthetics });
});
it('returns unchanged metadata if no summary is present', () => {
const metadataRaw = {
foo: [{ type: 'not_a_summary', help: 'foo help' }],
};
const metadata = {
foo: { type: 'not_a_summary', help: 'foo help' },
};
expect(fixSummariesMetadata(metadataRaw)).toEqual({ ...metadata, ...synthetics });
});
it('returns metadata with added count and sum for a summary', () => {
const metadata = {
foo: [{ type: 'not_a_summary', help: 'foo help' }],
bar: [{ type: 'summary', help: 'bar help' }],
};
const expected = {
foo: { type: 'not_a_summary', help: 'foo help' },
bar: { type: 'summary', help: 'bar help' },
bar_count: {
type: 'counter',
help: 'Count of events that have been observed for the base metric (bar help)',
},
bar_sum: { type: 'counter', help: 'Total sum of all observed values for the base metric (bar help)' },
};
expect(fixSummariesMetadata(metadata)).toEqual({ ...expected, ...synthetics });
});
it('returns metadata with added bucket/count/sum for a histogram', () => {
const metadata = {
foo: [{ type: 'not_a_histogram', help: 'foo help' }],
bar: [{ type: 'histogram', help: 'bar help' }],
};
const expected = {
foo: { type: 'not_a_histogram', help: 'foo help' },
bar: { type: 'histogram', help: 'bar help' },
bar_bucket: { type: 'counter', help: 'Cumulative counters for the observation buckets (bar help)' },
bar_count: {
type: 'counter',
help: 'Count of events that have been observed for the histogram metric (bar help)',
},
bar_sum: { type: 'counter', help: 'Total sum of all observed values for the histogram metric (bar help)' },
};
expect(fixSummariesMetadata(metadata)).toEqual({ ...expected, ...synthetics });
});
});
describe('expandRecordingRules()', () => {
it('returns query w/o recording rules as is', () => {
expect(expandRecordingRules('metric', {})).toBe('metric');
expect(expandRecordingRules('metric + metric', {})).toBe('metric + metric');
expect(expandRecordingRules('metric{}', {})).toBe('metric{}');
});
it('does not modify recording rules name in label values', () => {
expect(
expandRecordingRules('{__name__="metric"} + bar', {
metric: { expandedQuery: 'foo' },
bar: { expandedQuery: 'super' },
})
).toBe('{__name__="metric"} + super');
});
it('returns query with expanded recording rules', () => {
expect(expandRecordingRules('metric', { metric: { expandedQuery: 'foo' } })).toBe('foo');
expect(expandRecordingRules('metric + metric', { metric: { expandedQuery: 'foo' } })).toBe('foo + foo');
expect(expandRecordingRules('metric{}', { metric: { expandedQuery: 'foo' } })).toBe('foo{}');
expect(expandRecordingRules('metric[]', { metric: { expandedQuery: 'foo' } })).toBe('foo[]');
expect(
expandRecordingRules('metric + foo', {
metric: { expandedQuery: 'foo' },
foo: { expandedQuery: 'bar' },
})
).toBe('foo + bar');
});
it('returns query with labels with expanded recording rules', () => {
expect(
expandRecordingRules('metricA{label1="value1"} / metricB{label2="value2"}', {
metricA: { expandedQuery: 'fooA' },
metricB: { expandedQuery: 'fooB' },
})
).toBe('fooA{label1="value1"} / fooB{label2="value2"}');
expect(
expandRecordingRules('metricA{label1="value1",label2="value,2"}', {
metricA: { expandedQuery: 'rate(fooA[])' },
})
).toBe('rate(fooA{label1="value1", label2="value,2"}[])');
expect(
expandRecordingRules('metricA{label1="value1"} / metricB{label2="value2"}', {
metricA: { expandedQuery: 'rate(fooA[])' },
metricB: { expandedQuery: 'rate(fooB[])' },
})
).toBe('rate(fooA{label1="value1"}[]) / rate(fooB{label2="value2"}[])');
expect(
expandRecordingRules('metricA{label1="value1",label2="value2"} / metricB{label3="value3"}', {
metricA: { expandedQuery: 'rate(fooA[])' },
metricB: { expandedQuery: 'rate(fooB[])' },
})
).toBe('rate(fooA{label1="value1", label2="value2"}[]) / rate(fooB{label3="value3"}[])');
});
it('expands the query even it is wrapped with parentheses', () => {
expect(
expandRecordingRules('sum (metric{label1="value1"}) by (env)', {
metric: { expandedQuery: 'foo{labelInside="valueInside"}' },
})
).toBe('sum (foo{labelInside="valueInside", label1="value1"}) by (env)');
});
it('expands the query with regex match', () => {
expect(
expandRecordingRules('sum (metric{label1=~"/value1/(sa|sb)"}) by (env)', {
metric: { expandedQuery: 'foo{labelInside="valueInside"}' },
})
).toBe('sum (foo{labelInside="valueInside", label1=~"/value1/(sa|sb)"}) by (env)');
});
it('ins:metric:per{pid="val-42", comp="api"}', () => {
const query = `aaa:111{pid="val-42", comp="api"} + bbb:222{pid="val-42"}`;
const mapping = {
'aaa:111': {
expandedQuery:
'(max without (mp) (targetMetric{device=~"/dev/(sda1|sdb)"}) / max without (mp) (targetMetric2{device=~"/dev/(sda1|sdb)"}))',
},
'bbb:222': { expandedQuery: '(targetMetric2{device=~"/dev/(sda1|sdb)"})' },
};
const expected = `(max without (mp) (targetMetric{device=~"/dev/(sda1|sdb)", pid="val-42", comp="api"}) / max without (mp) (targetMetric2{device=~"/dev/(sda1|sdb)", pid="val-42", comp="api"})) + (targetMetric2{device=~"/dev/(sda1|sdb)", pid="val-42"})`;
const result = expandRecordingRules(query, mapping);
expect(result).toBe(expected);
});
it('when there is an identifier, identifier must be removed from expanded query', () => {
const query = `ins:metric:per{uuid="111", comp="api"}`;
const mapping = {
'ins:metric:per': {
expandedQuery: 'targetMetric{device="some_device"}',
identifier: 'uuid',
identifierValue: '111',
},
};
const expected = `targetMetric{device="some_device", comp="api"}`;
const result = expandRecordingRules(query, mapping);
expect(result).toBe(expected);
});
it('when there is an identifier, identifier must be removed from complex expanded query', () => {
const query = `instance_path:requests:rate5m{uuid="111", four="tops"} + instance_path:requests:rate15m{second="album", uuid="222"}`;
const mapping = {
'instance_path:requests:rate5m': {
expandedQuery: `rate(prometheus_http_requests_total{job="prometheus"}`,
identifier: 'uuid',
identifierValue: '111',
},
'instance_path:requests:rate15m': {
expandedQuery: `prom_http_requests_sum{job="prometheus"}`,
identifier: 'uuid',
identifierValue: '222',
},
};
const expected = `rate(prometheus_http_requests_total{job="prometheus", four="tops"} + prom_http_requests_sum{job="prometheus", second="album"}`;
const result = expandRecordingRules(query, mapping);
expect(result).toBe(expected);
});
it('when there is an empty label value it should still be able to expand the rule', () => {
const query = `sum(max by (cluster, container) (pod_cpu:active:kube_limits{container!="", cluster=~"pink"}))`;
const mapping = {
'pod_cpu:active:kube_limits': {
expandedQuery: `kube_limits{job!="", resource="cpu"} * on (namespace, pod, cluster) group_left () max by (namespace, pod, cluster) ((kube_pod_status_phase{phase=~"Pending|Running"} == 1))`,
},
};
const expected = `sum(max by (cluster, container) (kube_limits{job!="", resource="cpu", container!="", cluster=~"pink"} * on (namespace, pod, cluster) group_left () max by (namespace, pod, cluster) ((kube_pod_status_phase{phase=~"Pending|Running", container!="", cluster=~"pink"} == 1))))`;
const result = expandRecordingRules(query, mapping);
expect(result).toBe(expected);
});
});
describe('escapeLabelValueInExactSelector()', () => {
it('handles newline characters', () => {
expect(escapeLabelValueInExactSelector('t\nes\nt')).toBe('t\\nes\\nt');
});
it('handles backslash characters', () => {
expect(escapeLabelValueInExactSelector('t\\es\\t')).toBe('t\\\\es\\\\t');
});
it('handles double-quote characters', () => {
expect(escapeLabelValueInExactSelector('t"es"t')).toBe('t\\"es\\"t');
});
it('handles all together', () => {
expect(escapeLabelValueInExactSelector('t\\e"st\nl\nab"e\\l')).toBe('t\\\\e\\"st\\nl\\nab\\"e\\\\l');
});
});
describe('escapeLabelValueInRegexSelector()', () => {
it('handles newline characters', () => {
expect(escapeLabelValueInRegexSelector('t\nes\nt')).toBe('t\\nes\\nt');
});
it('handles backslash characters', () => {
expect(escapeLabelValueInRegexSelector('t\\es\\t')).toBe('t\\\\\\\\es\\\\\\\\t');
});
it('handles double-quote characters', () => {
expect(escapeLabelValueInRegexSelector('t"es"t')).toBe('t\\"es\\"t');
});
it('handles regex-meaningful characters', () => {
expect(escapeLabelValueInRegexSelector('t+es$t')).toBe('t\\\\+es\\\\$t');
});
it('handles all together', () => {
expect(escapeLabelValueInRegexSelector('t\\e"s+t\nl\n$ab"e\\l')).toBe(
't\\\\\\\\e\\"s\\\\+t\\nl\\n\\\\$ab\\"e\\\\\\\\l'
);
});
});
describe('getRangeSnapInterval', () => {
it('will not change input if set to no cache', () => {
const intervalSeconds = 10 * 60; // 10 minutes
const now = new Date().valueOf();
const expectedFrom = dateTime(now - intervalSeconds * 1000);
const expectedTo = dateTime(now);
const range: TimeRange = {
from: expectedFrom,
to: expectedTo,
} as TimeRange;
expect(getRangeSnapInterval(PrometheusCacheLevel.None, range)).toEqual({
start: getPrometheusTime(expectedFrom, false).toString(),
end: getPrometheusTime(expectedTo, true).toString(),
});
});
it('will snap range to closest minute', () => {
const queryDurationMinutes = 10;
const intervalSeconds = queryDurationMinutes * 60; // 10 minutes
const now = 1680901009826;
const nowPlusOneMinute = now + 1000 * 60;
const nowPlusTwoMinute = now + 1000 * 60 * 2;
const nowTime = dateTime(now) as Moment;
const expectedFrom = nowTime.clone().startOf('minute').subtract(queryDurationMinutes, 'minute');
const expectedTo = nowTime.clone().startOf('minute').add(1, 'minute');
const range: TimeRange = {
from: dateTime(now - intervalSeconds * 1000),
to: dateTime(now),
} as TimeRange;
const range2: TimeRange = {
from: dateTime(nowPlusOneMinute - intervalSeconds * 1000),
to: dateTime(nowPlusOneMinute),
raw: {
from: dateTime(nowPlusOneMinute - intervalSeconds * 1000),
to: dateTime(nowPlusOneMinute),
},
};
const range3: TimeRange = {
from: dateTime(nowPlusTwoMinute - intervalSeconds * 1000),
to: dateTime(nowPlusTwoMinute),
raw: {
from: dateTime(nowPlusTwoMinute - intervalSeconds * 1000),
to: dateTime(nowPlusTwoMinute),
},
};
const first = getRangeSnapInterval(PrometheusCacheLevel.Low, range);
const second = getRangeSnapInterval(PrometheusCacheLevel.Low, range2);
const third = getRangeSnapInterval(PrometheusCacheLevel.Low, range3);
expect(first).toEqual({
start: getPrometheusTime(expectedFrom as DateTime, false).toString(10),
end: getPrometheusTime(expectedTo as DateTime, false).toString(10),
});
expect(second).toEqual({
start: getPrometheusTime(expectedFrom.clone().add(1, 'minute') as DateTime, false).toString(10),
end: getPrometheusTime(expectedTo.clone().add(1, 'minute') as DateTime, false).toString(10),
});
expect(third).toEqual({
start: getPrometheusTime(expectedFrom.clone().add(2, 'minute') as DateTime, false).toString(10),
end: getPrometheusTime(expectedTo.clone().add(2, 'minute') as DateTime, false).toString(10),
});
});
it('will snap range to closest 10 minute', () => {
const queryDurationMinutes = 60;
const intervalSeconds = queryDurationMinutes * 60; // 10 minutes
const now = 1680901009826;
const nowPlusOneMinute = now + 1000 * 60;
const nowPlusTwoMinute = now + 1000 * 60 * 2;
const nowTime = dateTime(now) as Moment;
const nowTimePlusOne = dateTime(nowPlusOneMinute) as Moment;
const nowTimePlusTwo = dateTime(nowPlusTwoMinute) as Moment;
const calculateClosest10 = (date: Moment): Moment => {
const numberOfMinutes = Math.floor(date.minutes() / 10) * 10;
const numberOfHours = numberOfMinutes < 60 ? date.hours() : date.hours() + 1;
return date
.clone()
.minutes(numberOfMinutes % 60)
.hours(numberOfHours);
};
const expectedFromFirst = calculateClosest10(
nowTime.clone().startOf('minute').subtract(queryDurationMinutes, 'minute')
);
const expectedToFirst = calculateClosest10(nowTime.clone().startOf('minute').add(1, 'minute'));
const expectedFromSecond = calculateClosest10(
nowTimePlusOne.clone().startOf('minute').subtract(queryDurationMinutes, 'minute')
);
const expectedToSecond = calculateClosest10(nowTimePlusOne.clone().startOf('minute').add(1, 'minute'));
const expectedFromThird = calculateClosest10(
nowTimePlusTwo.clone().startOf('minute').subtract(queryDurationMinutes, 'minute')
);
const expectedToThird = calculateClosest10(nowTimePlusTwo.clone().startOf('minute').add(1, 'minute'));
const range: TimeRange = {
from: dateTime(now - intervalSeconds * 1000),
to: dateTime(now),
} as TimeRange;
const range2: TimeRange = {
from: dateTime(nowPlusOneMinute - intervalSeconds * 1000),
to: dateTime(nowPlusOneMinute),
raw: {
from: dateTime(nowPlusOneMinute - intervalSeconds * 1000),
to: dateTime(nowPlusOneMinute),
},
};
const range3: TimeRange = {
from: dateTime(nowPlusTwoMinute - intervalSeconds * 1000),
to: dateTime(nowPlusTwoMinute),
raw: {
from: dateTime(nowPlusTwoMinute - intervalSeconds * 1000),
to: dateTime(nowPlusTwoMinute),
},
};
const first = getRangeSnapInterval(PrometheusCacheLevel.Medium, range);
const second = getRangeSnapInterval(PrometheusCacheLevel.Medium, range2);
const third = getRangeSnapInterval(PrometheusCacheLevel.Medium, range3);
expect(first).toEqual({
start: getPrometheusTime(expectedFromFirst as DateTime, false).toString(10),
end: getPrometheusTime(expectedToFirst as DateTime, false).toString(10),
});
expect(second).toEqual({
start: getPrometheusTime(expectedFromSecond.clone() as DateTime, false).toString(10),
end: getPrometheusTime(expectedToSecond.clone() as DateTime, false).toString(10),
});
expect(third).toEqual({
start: getPrometheusTime(expectedFromThird.clone() as DateTime, false).toString(10),
end: getPrometheusTime(expectedToThird.clone() as DateTime, false).toString(10),
});
});
it('will snap range to closest 60 minute', () => {
const queryDurationMinutes = 120;
const intervalSeconds = queryDurationMinutes * 60;
const now = 1680901009826;
const nowPlusOneMinute = now + 1000 * 60;
const nowPlusTwoMinute = now + 1000 * 60 * 2;
const nowTime = dateTime(now) as Moment;
const nowTimePlusOne = dateTime(nowPlusOneMinute) as Moment;
const nowTimePlusTwo = dateTime(nowPlusTwoMinute) as Moment;
const calculateClosest60 = (date: Moment): Moment => {
const numberOfMinutes = Math.floor(date.minutes() / 60) * 60;
const numberOfHours = numberOfMinutes < 60 ? date.hours() : date.hours() + 1;
return date
.clone()
.minutes(numberOfMinutes % 60)
.hours(numberOfHours);
};
const expectedFromFirst = calculateClosest60(
nowTime.clone().startOf('minute').subtract(queryDurationMinutes, 'minute')
);
const expectedToFirst = calculateClosest60(nowTime.clone().startOf('minute').add(1, 'minute'));
const expectedFromSecond = calculateClosest60(
nowTimePlusOne.clone().startOf('minute').subtract(queryDurationMinutes, 'minute')
);
const expectedToSecond = calculateClosest60(nowTimePlusOne.clone().startOf('minute').add(1, 'minute'));
const expectedFromThird = calculateClosest60(
nowTimePlusTwo.clone().startOf('minute').subtract(queryDurationMinutes, 'minute')
);
const expectedToThird = calculateClosest60(nowTimePlusTwo.clone().startOf('minute').add(1, 'minute'));
const range: TimeRange = {
from: dateTime(now - intervalSeconds * 1000),
to: dateTime(now),
} as TimeRange;
const range2: TimeRange = {
from: dateTime(nowPlusOneMinute - intervalSeconds * 1000),
to: dateTime(nowPlusOneMinute),
raw: {
from: dateTime(nowPlusOneMinute - intervalSeconds * 1000),
to: dateTime(nowPlusOneMinute),
},
};
const range3: TimeRange = {
from: dateTime(nowPlusTwoMinute - intervalSeconds * 1000),
to: dateTime(nowPlusTwoMinute),
raw: {
from: dateTime(nowPlusTwoMinute - intervalSeconds * 1000),
to: dateTime(nowPlusTwoMinute),
},
};
const first = getRangeSnapInterval(PrometheusCacheLevel.High, range);
const second = getRangeSnapInterval(PrometheusCacheLevel.High, range2);
const third = getRangeSnapInterval(PrometheusCacheLevel.High, range3);
expect(first).toEqual({
start: getPrometheusTime(expectedFromFirst as DateTime, false).toString(10),
end: getPrometheusTime(expectedToFirst as DateTime, false).toString(10),
});
expect(second).toEqual({
start: getPrometheusTime(expectedFromSecond.clone() as DateTime, false).toString(10),
end: getPrometheusTime(expectedToSecond.clone() as DateTime, false).toString(10),
});
expect(third).toEqual({
start: getPrometheusTime(expectedFromThird.clone() as DateTime, false).toString(10),
end: getPrometheusTime(expectedToThird.clone() as DateTime, false).toString(10),
});
});
});
describe('toPromLikeQuery', () => {
it('export abstract query to PromQL-like query', () => {
const abstractQuery: AbstractQuery = {
refId: 'bar',
labelMatchers: [
{ name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' },
{ name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' },
{ name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' },
{ name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' },
],
};
expect(toPromLikeQuery(abstractQuery)).toMatchObject({
refId: 'bar',
expr: '{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}',
range: true,
});
});
});
describe('truncateResult', () => {
it('truncates array longer then 1k from the start of array', () => {
// creates an array of 1k + 1 elements with values from 0 to 1k
const array = Array.from(Array(1001).keys());
expect(array[1000]).toBe(1000);
truncateResult(array);
expect(array.length).toBe(1000);
expect(array[0]).toBe(0);
expect(array[999]).toBe(999);
});
});
describe('removeQuotesIfExist', () => {
it('removes quotes from a string with double quotes', () => {
const input = '"hello"';
const result = removeQuotesIfExist(input);
expect(result).toBe('hello');
});
it('returns the original string if it does not start and end with quotes', () => {
const input = 'hello';
const result = removeQuotesIfExist(input);
expect(result).toBe('hello');
});
it('returns the original string if it has mismatched quotes', () => {
const input = '"hello';
const result = removeQuotesIfExist(input);
expect(result).toBe('"hello');
});
it('removes quotes for strings with special characters inside quotes', () => {
const input = '"hello, world!"';
const result = removeQuotesIfExist(input);
expect(result).toBe('hello, world!');
});
it('removes quotes for strings with spaces inside quotes', () => {
const input = '" "';
const result = removeQuotesIfExist(input);
expect(result).toBe(' ');
});
it('returns the original string for an empty string', () => {
const input = '';
const result = removeQuotesIfExist(input);
expect(result).toBe('');
});
it('returns the original string if the string only has a single quote character', () => {
const input = '"';
const result = removeQuotesIfExist(input);
expect(result).toBe('"');
});
it('handles strings with nested quotes correctly', () => {
const input = '"nested \"quotes\""';
const result = removeQuotesIfExist(input);
expect(result).toBe('nested \"quotes\"');
});
it('removes quotes from a numeric string wrapped in quotes', () => {
const input = '"12345"';
const result = removeQuotesIfExist(input);
expect(result).toBe('12345');
});
});

View File

@@ -1,420 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_utils.ts
import { invert } from 'lodash';
import { Token } from 'prismjs';
import {
type AbstractLabelMatcher,
AbstractLabelOperator,
type AbstractQuery,
type DataQuery,
dateMath,
type DateTime,
incrRoundDn,
type TimeRange,
} from '@grafana/data';
import { addLabelToQuery } from './add_label_to_query';
import { getCacheDurationInMinutes } from './caching';
import { PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './constants';
import {
PrometheusCacheLevel,
type PromMetricsMetadata,
type PromMetricsMetadataItem,
type RecordingRuleIdentifier,
} from './types';
export const processHistogramMetrics = (metrics: string[]) => {
const resultSet: Set<string> = new Set();
const regexp = new RegExp('_bucket($|:)');
for (let index = 0; index < metrics.length; index++) {
const metric = metrics[index];
const isHistogramValue = regexp.test(metric);
if (isHistogramValue) {
resultSet.add(metric);
}
}
return [...resultSet];
};
// This will capture 4 groups. Example label filter => {instance="10.4.11.4:9003"}
// 1. label: instance
// 2. operator: =
// 3. value: "10.4.11.4:9003"
// 4. comma: if there is a comma it will give ,
// 5. space: if there is a space after comma it will give the whole space
// comma and space is useful for addLabelsToExpression function
const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")(,)?(\s*)?/g;
export function expandRecordingRules(query: string, mapping: { [name: string]: RecordingRuleIdentifier }): string {
const getRuleRegex = (ruleName: string) => new RegExp(`(\\s|\\(|^)(${ruleName})(\\s|$|\\(|\\[|\\{)`, 'ig');
// For each mapping key we iterate over the query and split them in parts.
// recording:rule{label=~"/label/value"} * some:other:rule{other_label="value"}
// We want to keep parts in here like this:
// recording:rule
// {label=~"/label/value"} *
// some:other:rule
// {other_label="value"}
const tmpSplitParts = Object.keys(mapping).reduce<string[]>(
(prev, curr) => {
let parts: string[] = [];
let tmpParts: string[] = [];
let removeIdx: number[] = [];
// we iterate over prev because it might be like this after first loop
// recording:rule and {label=~"/label/value"} * some:other:rule{other_label="value"}
// so we need to split the second part too
prev.filter(Boolean).forEach((p, i) => {
const doesMatch = p.match(getRuleRegex(curr));
if (doesMatch) {
parts = p.split(curr);
if (parts.length === 2) {
// this is the case when we have such result for this query
// max (metric{label="value"})
// "max(", "{label="value"}"
removeIdx.push(i);
tmpParts.push(...[parts[0], curr, parts[1]].filter(Boolean));
} else if (parts.length > 2) {
// this is the case when we have such query
// metric + metric
// when we split it we have such data
// "", " + ", ""
removeIdx.push(i);
parts = parts.map((p) => (p === '' ? curr : p));
tmpParts.push(...parts);
}
}
});
// if we have idx to remove that means we split the value in that index.
// No need to keep it. Have the new split values instead.
removeIdx.forEach((ri) => (prev[ri] = ''));
prev = prev.filter(Boolean);
prev.push(...tmpParts);
return prev;
},
[query]
);
// we have the separate parts. we need to replace the metric and apply the labels if there is any
let labelFound = false;
const trulyExpandedQuery = tmpSplitParts.map((tsp, i) => {
// if we know this loop tsp is a label, not the metric we want to expand
if (labelFound) {
labelFound = false;
return '';
}
// check if the mapping is there
if (mapping[tsp]) {
const { expandedQuery: recordingRule, identifierValue, identifier } = mapping[tsp];
// it is a recording rule. if the following is a label then apply it
if (i + 1 !== tmpSplitParts.length && tmpSplitParts[i + 1].match(labelRegexp)) {
// the next value in the loop is label. Let's apply labels to the metric
labelFound = true;
const regexp = new RegExp(`(,)?(\\s)?(${identifier}=\\"${identifierValue}\\")(,)?(\\s)?`, 'g');
const labels = tmpSplitParts[i + 1].replace(regexp, '');
const invalidLabelsRegex = /(\)\{|\}\{|\]\{)/;
return addLabelsToExpression(recordingRule + labels, invalidLabelsRegex);
} else {
// it is not a recording rule and might be a binary operation in between two recording rules
// So no need to do anything. just return it.
return recordingRule;
}
}
return tsp;
});
// Remove empty strings and merge them
return trulyExpandedQuery.filter(Boolean).join('');
}
function addLabelsToExpression(expr: string, invalidLabelsRegexp: RegExp) {
const match = expr.match(invalidLabelsRegexp);
if (!match) {
return expr;
}
// Split query into 2 parts - before the invalidLabelsRegex match and after.
const indexOfRegexMatch = match.index ?? 0;
const exprBeforeRegexMatch = expr.slice(0, indexOfRegexMatch + 1);
const exprAfterRegexMatch = expr.slice(indexOfRegexMatch + 1);
// Create arrayOfLabelObjects with label objects that have key, operator and value.
const arrayOfLabelObjects: Array<{
key: string;
operator: string;
value: string;
comma?: string;
space?: string;
}> = [];
exprAfterRegexMatch.replace(labelRegexp, (label, key, operator, value, comma, space) => {
arrayOfLabelObjects.push({ key, operator, value, comma, space });
return '';
});
// Loop through all label objects and add them to query.
// As a starting point we have valid query without the labels.
let result = exprBeforeRegexMatch;
arrayOfLabelObjects.filter(Boolean).forEach((obj) => {
// Remove extra set of quotes from obj.value
const value = obj.value.slice(1, -1);
result = addLabelToQuery(result, obj.key, value, obj.operator);
});
// reconstruct the labels
let existingLabel = arrayOfLabelObjects.reduce((prev, curr) => {
prev += `${curr.key}${curr.operator}${curr.value}${curr.comma ?? ''}${curr.space ?? ''}`;
return prev;
}, '');
// Check if there is anything besides labels
// Useful for this kind of metrics sum (recording_rule_metric{label1="value1"}) by (env)
// if we don't check this part, ) by (env) part will be lost
existingLabel = '{' + existingLabel + '}';
const potentialLeftOver = exprAfterRegexMatch.replace(existingLabel, '');
return result + potentialLeftOver;
}
/**
* Adds metadata for synthetic metrics for which the API does not provide metadata.
* See https://github.com/grafana/grafana/issues/22337 for details.
*
* @param metadata HELP and TYPE metadata from /api/v1/metadata
*/
export function fixSummariesMetadata(metadata: { [metric: string]: PromMetricsMetadataItem[] }): PromMetricsMetadata {
if (!metadata) {
return metadata;
}
const baseMetadata: PromMetricsMetadata = {};
const summaryMetadata: PromMetricsMetadata = {};
for (const metric in metadata) {
// NOTE: based on prometheus-documentation, we can receive
// multiple metadata-entries for the given metric, it seems
// it happens when the same metric is on multiple targets
// and their help-text differs
// (https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metric-metadata)
// for now we just use the first entry.
const item = metadata[metric][0];
baseMetadata[metric] = item;
if (item.type === 'histogram') {
summaryMetadata[`${metric}_bucket`] = {
type: 'counter',
help: `Cumulative counters for the observation buckets (${item.help})`,
};
summaryMetadata[`${metric}_count`] = {
type: 'counter',
help: `Count of events that have been observed for the histogram metric (${item.help})`,
};
summaryMetadata[`${metric}_sum`] = {
type: 'counter',
help: `Total sum of all observed values for the histogram metric (${item.help})`,
};
}
if (item.type === 'summary') {
summaryMetadata[`${metric}_count`] = {
type: 'counter',
help: `Count of events that have been observed for the base metric (${item.help})`,
};
summaryMetadata[`${metric}_sum`] = {
type: 'counter',
help: `Total sum of all observed values for the base metric (${item.help})`,
};
}
}
// Synthetic series
const syntheticMetadata: PromMetricsMetadata = {};
syntheticMetadata['ALERTS'] = {
type: 'gauge',
help: 'Time series showing pending and firing alerts. The sample value is set to 1 as long as the alert is in the indicated active (pending or firing) state.',
};
return { ...baseMetadata, ...summaryMetadata, ...syntheticMetadata };
}
export function roundMsToMin(milliseconds: number): number {
return roundSecToMin(milliseconds / 1000);
}
function roundSecToMin(seconds: number): number {
return Math.floor(seconds / 60);
}
// Returns number of minutes rounded up to the nearest nth minute
function roundSecToNextMin(seconds: number, secondsToRound = 1): number {
return Math.ceil(seconds / 60) - (Math.ceil(seconds / 60) % secondsToRound);
}
const FromPromLikeMap: Record<string, AbstractLabelOperator> = {
'=': AbstractLabelOperator.Equal,
'!=': AbstractLabelOperator.NotEqual,
'=~': AbstractLabelOperator.EqualRegEx,
'!~': AbstractLabelOperator.NotEqualRegEx,
};
const ToPromLikeMap: Record<AbstractLabelOperator, string> = invert(FromPromLikeMap) as Record<
AbstractLabelOperator,
string
>;
function toPromLikeExpr(labelBasedQuery: AbstractQuery): string {
const expr = labelBasedQuery.labelMatchers
.map((selector: AbstractLabelMatcher) => {
const operator = ToPromLikeMap[selector.operator];
if (operator) {
return `${selector.name}${operator}"${selector.value}"`;
} else {
return '';
}
})
.filter((e: string) => e !== '')
.join(', ');
return expr ? `{${expr}}` : '';
}
export function toPromLikeQuery(labelBasedQuery: AbstractQuery): PromLikeQuery {
return {
refId: labelBasedQuery.refId,
expr: toPromLikeExpr(labelBasedQuery),
range: true,
};
}
interface PromLikeQuery extends DataQuery {
expr: string;
range: boolean;
}
function getMaybeTokenStringContent(token: Token): string {
if (typeof token.content === 'string') {
return token.content;
}
return '';
}
export function extractLabelMatchers(tokens: Array<string | Token>): AbstractLabelMatcher[] {
const labelMatchers: AbstractLabelMatcher[] = [];
for (const token of tokens) {
if (!(token instanceof Token)) {
continue;
}
if (token.type === 'context-labels') {
let labelKey = '';
let labelValue = '';
let labelOperator = '';
const contentTokens = Array.isArray(token.content) ? token.content : [token.content];
for (let currentToken of contentTokens) {
if (typeof currentToken === 'string') {
let currentStr: string;
currentStr = currentToken;
if (currentStr === '=' || currentStr === '!=' || currentStr === '=~' || currentStr === '!~') {
labelOperator = currentStr;
}
} else if (currentToken instanceof Token) {
switch (currentToken.type) {
case 'label-key':
labelKey = getMaybeTokenStringContent(currentToken);
break;
case 'label-value':
labelValue = getMaybeTokenStringContent(currentToken);
labelValue = labelValue.substring(1, labelValue.length - 1);
const labelComparator = FromPromLikeMap[labelOperator];
if (labelComparator) {
labelMatchers.push({ name: labelKey, operator: labelComparator, value: labelValue });
}
break;
}
}
}
}
}
return labelMatchers;
}
/**
* Calculates new interval "snapped" to the closest Nth minute, depending on cache level datasource setting
* @param cacheLevel
* @param range
*/
export function getRangeSnapInterval(
cacheLevel: PrometheusCacheLevel,
range: TimeRange
): {
start: string;
end: string;
} {
// Don't round the range if we're not caching
if (cacheLevel === PrometheusCacheLevel.None) {
return {
start: getPrometheusTime(range.from, false).toString(),
end: getPrometheusTime(range.to, true).toString(),
};
}
// Otherwise round down to the nearest nth minute for the start time
const startTime = getPrometheusTime(range.from, false);
const startTimeQuantizedSeconds = incrRoundDn(startTime, getCacheDurationInMinutes(cacheLevel) * 60);
// And round up to the nearest nth minute for the end time
const endTime = getPrometheusTime(range.to, true);
const endTimeQuantizedSeconds = roundSecToNextMin(endTime, getCacheDurationInMinutes(cacheLevel)) * 60;
// If the interval was too short, we could have rounded both start and end to the same time, if so let's add one step to the end
if (startTimeQuantizedSeconds === endTimeQuantizedSeconds) {
const endTimePlusOneStep = endTimeQuantizedSeconds + getCacheDurationInMinutes(cacheLevel) * 60;
return { start: startTimeQuantizedSeconds.toString(), end: endTimePlusOneStep.toString() };
}
const start = startTimeQuantizedSeconds.toString();
const end = endTimeQuantizedSeconds.toString();
return { start, end };
}
export function getPrometheusTime(date: string | DateTime, roundUp: boolean) {
if (typeof date === 'string') {
date = dateMath.parse(date, roundUp)!;
}
return Math.ceil(date.valueOf() / 1000);
}
/**
* Used to truncate metrics, label names and label value in the query builder select components
* to improve frontend performance. This is best used with an async select component including
* the loadOptions property where we should still allow users to search all results with a string.
* This can be done either storing the total results or querying an api that allows for matching a query.
*
* @param array
* @param limit
* @returns
*/
export function truncateResult<T>(array: T[], limit?: number): T[] {
if (limit === undefined) {
limit = PROMETHEUS_QUERY_BUILDER_MAX_RESULTS;
}
array.length = Math.min(array.length, limit);
return array;
}
/**
* Removes quotes from a string if they exist.
* Used to handle utf8 label keys in Prometheus queries.
*
* @param {string} input - Input string that may have surrounding quotes
* @returns {string} String with surrounding quotes removed if they existed
*/
export function removeQuotesIfExist(input: string): string {
const match = input.match(/^"(.*)"$/); // extract the content inside the quotes
return match?.[1] ?? input;
}

View File

@@ -1,11 +0,0 @@
import { LANGUAGES, type ResourceLoader, type Resources } from '@grafana/i18n';
const resources = LANGUAGES.reduce<Record<string, () => Promise<{ default: Resources }>>>((acc, lang) => {
acc[lang.code] = async () => await import(`./locales/${lang.code}/grafana-prometheus.json`);
return acc;
}, {});
export const loadResources: ResourceLoader = async (resolvedLanguage: string) => {
const translation = await resources[resolvedLanguage]();
return translation.default;
};

View File

@@ -1,492 +0,0 @@
{
"grafana-prometheus": {
"components": {
"annotation-query-editor": {
"annotation-data-load-error": "Chyba načtení dat vysvětlivky!",
"aria-label-lower-limit-parameter": "Nastavit dolní limit pro parametr kroku",
"label-min-step": "Min. krok",
"label-series-value-as-timestamp": "Hodnota řady jako časové razítko",
"label-tags": "Tagy",
"label-text": "Text",
"label-title": "Název",
"placeholder-auto": "auto.",
"tooltip-either-pattern-example-instance-replaced-label": "Použijte buď název, nebo vzor. Například {{labelTemplate}} je nahrazeno hodnotou štítku pro štítek {{labelName}}.",
"tooltip-min-step": "Dodatečný dolní limit pro parametr kroku dotazu Prometheus a pro proměnné <2>{{intervalVar}}</2> a <4>{{rateIntervalVar}}</4>.",
"tooltip-timestamp-milliseconds-series-value-seconds-multiply": "Jednotka časového razítka je milisekunda. Pokud je jednotkou hodnoty řady sekunda, vynásobte její vektor rozsahu 1 000."
},
"get-query-type-options": {
"description": {
"instant-query-range": "Spustit okamžitý dotaz a dotaz na rozsah"
},
"label": {
"both": "Oba"
},
"range-options": {
"description": {
"query-range": "Spustit dotaz v časovém rozsahu"
},
"label": {
"instant": "Instantní",
"range": "Rozsah"
}
}
},
"label-selector": {
"aria-label-filter-expression-for-label": "Filtrovat výraz pro štítek",
"description-select-labels": "Po výběru hodnot štítků se zobrazí pouze možné kombinace štítků.",
"select-labels-to-search-in": "2. Vyberte štítky, ve kterých chcete hledat"
},
"metric-selector": {
"aria-label-filter-expression-for-metric": "Filtrovat výraz pro metriku",
"aria-label-limit-results-from-series-endpoint": "Omezit výsledky z koncového bodu řady",
"description-series-limit": "Limit se vztahuje na všechny metriky, štítky a hodnoty. Ponechte pole prázdné pro použití výchozího limitu. Nastavením na 0 zakážete limit a načtete vše  může to způsobit problémy s výkonem.",
"label-select-metric": "Po výběru metriky se zobrazí pouze možné štítky. Štítky jsou omezeny limitem řady níže.",
"select-a-metric": "1. Vyberte metriku",
"series-limit": "Limit řady"
},
"prom-cheat-sheet": {
"prom-ql-cheat-sheet": "Přehled PromQL"
},
"prom-exemplar-field": {
"exemplars": "Příklady",
"tooltip-disable-query": "Zakázat dotaz s příklady",
"tooltip-enable-query": "Povolit dotaz s příklady"
},
"prom-explore-extra-field": {
"aria-label-prometheus-extra-field": "Extra pole Prometheus",
"aria-label-query-type-field": "Pole typu dotazu",
"aria-label-step-field": "Pole kroku",
"min-step": "Min. krok",
"query-type": "Typ dotazu",
"tooltip-units-builtin-variables-example-interval-rateinterval": "Zde lze použít časové jednotky a vestavěné proměnné, například: {{example1}}, {{example2}}, {{example3}} {{example4}}, {{example5}}, {{example6}}, {{example7}} (výchozí, pokud není zadána žádná jednotka: {{default}})"
},
"prom-query-field": {
"placeholder-enter-a-prom-ql-query": "Zadejte dotaz PromQL…"
},
"prom-variable-query-editor": {
"aria-label-classic-query": "Klasický dotaz",
"aria-label-metric-regex": "Metrický regulární výraz",
"aria-label-metric-selector": "Volič metriky",
"aria-label-prometheus-query": "Dotaz Prometheus",
"aria-label-query-type": "Typ dotazu",
"aria-label-series-query": "Dotaz na řadu",
"label-classic-query": "Klasický dotaz",
"label-label": "Štítek",
"label-metric-regex": "Metrický regulární výraz",
"label-query": "Dotaz",
"label-query-type": "Typ dotazu",
"label-series-query": "Dotaz na řadu",
"placeholder-classic-query": "Klasický dotaz",
"placeholder-metric-regex": "Metrický regulární výraz",
"placeholder-prometheus-query": "Dotaz Prometheus",
"placeholder-select-query-type": "Vyberte typ dotazu",
"placeholder-series-query": "Dotaz na řadu",
"returns-metrics-matching-specified-metric-regex": "Vrátí seznam metrik odpovídajících zadanému metrickému regulárnímu výrazu.",
"tooltip-classic-query": "Původní implementace editoru proměnných dotazů Prometheus. Zadejte řetězec se správným typem dotazu a parametry, jak je popsáno v těchto dokumentech. Například {{exampleQuery}}.",
"tooltip-label": "Vrátí seznam hodnot štítků pro název štítku ve všech metrikách, pokud není metrika zadána.",
"tooltip-metric-regex": "Vrátí seznam názvů štítků, volitelně filtrování podle zadaného metrického regulárního výrazu.",
"tooltip-query": "Vrátí seznam výsledků dotazu Prometheus pro daný dotaz. To může zahrnovat funkce Prometheus, tj. {{exampleQuery}}.",
"tooltip-query-type": "Plugin zdroje dat Prometheus poskytuje následující typy dotazů pro proměnné šablony.",
"tooltip-series-query": "Zadejte metriku se štítky, pouze metriku nebo pouze štítky, tj. {{example1}}, {{example2}} nebo {{example3}}. Vrátí seznam časových řad spojených se zadanými daty."
},
"selector-actions": {
"aria-label-selector": "volič",
"aria-label-selector-clear-button": "Tlačítko Vymazat volič",
"aria-label-use-selector-as-metrics-button": "Tlačítko Použít volič jako metriku",
"aria-label-use-selector-for-query-button": "Tlačítko Použít volič pro dotaz",
"aria-label-validate-submit-button": "Tlačítko Ověřit odeslání",
"clear": "Vymazat",
"resulting-selector": "4. Výsledný volič",
"use-as-rate-query": "Použít jako dotaz na sazbu",
"use-query": "Použít dotaz",
"validate-selector": "Ověřit volič"
},
"value-selector": {
"aria-label-filter-expression-for-label-values": "Filtrovat výraz pro hodnoty štítků",
"aria-label-values-for": "Hodnoty pro {{labelKey}}",
"description-search-field-values-across-selected-labels": "Použijte vyhledávací pole pro hledání hodnot napříč vybranými štítky.",
"select-multiple-values-for-your-labels": "3. Vyberte (více) hodnot pro štítky"
}
},
"configuration": {
"alerting-settings-overhaul": {
"label-allow-as-recording-rules-target": "Povolit jako cíl pravidel nahrávání",
"label-manage-alerts-via-alerting-ui": "Spravovat výstrahy prostřednictvím uživatelského rozhraní upozorňování",
"title-alerting": "Výstrahy",
"tooltip-allow-as-recording-rules-target": "Povolit výběr tohoto zdroje dat jako cíle pro zápis pravidel nahrávání.",
"tooltip-manage-alerts-via-alerting-ui": "Spravovat pravidla výstrah pro tento zdroj dat. Chcete-li spravovat další zdroje Alerting, přidejte zdroj dat Alertmanager."
},
"config-editor": {
"browser-access-mode-error": "Režim přístupu z prohlížeče ve zdroji dat Prometheus již není k dispozici. Přepněte na režim přístupu k serveru.",
"description-advanced-settings": "Další nastavení jsou volitelná nastavení, která lze nakonfigurovat pro větší kontrolu nad zdrojem dat.",
"title-advanced-settings": "Pokročilá nastavení",
"title-error": "Chyba"
},
"data-source-http-settings-overhaul": {
"tooltip-browser-access-mode": "Vaše metoda přístupu je <1>Prohlížeč</1>, to znamená, že adresa URL musí být přístupná z prohlížeče.",
"tooltip-http-url": "Zadejte úplnou adresu URL HTTP (například {{exampleURL}})",
"tooltip-server-access-mode": "Vaše metoda přístupu je <1>Server</1>, to znamená, že adresa URL musí být přístupná z backendu/serveru Grafana."
},
"docs-tip": {
"visit-docs-for-more-details-here": "Další podrobnosti najdete v dokumentaci zde."
},
"exemplar-setting": {
"label-data-source": "Zdroj dat",
"label-internal-link": "Interní odkaz",
"label-label-name": "Název štítku",
"label-remove-exemplar-link": "Odebrat příklad odkazu",
"label-url": "URL",
"label-url-label": "Štítek adresy URL",
"placeholder-go-to-examplecom": "Přejít na example.com",
"placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}",
"placeholder-trace-id": "traceID",
"title-remove-exemplar-link": "Odebrat příklad odkazu",
"tooltip-data-source": "Zdroj dat, na který bude exemplář odkazovat.",
"tooltip-internal-link": "Tuto možnost povolte, pokud máte interní odkaz. Pokud je tato možnost povolena, zobrazí se výběr zdroje dat. Vyberte úložiště dat pro sledování backendu pro vaše vzorová data.",
"tooltip-label-name": "Název pole v objektu štítků, které by mělo být použito k získání traceID.",
"tooltip-url": "Adresa URL backendu stopy pro uživatele za účelem trasování",
"tooltip-url-label": "Použijte k přepsání štítku tlačítka v poli traceID exempláře."
},
"exemplars-settings": {
"add": "Přidat",
"no-exemplars-configurations": "Žádné příklady konfigurace",
"title-exemplars": "Příklady"
},
"prom-settings": {
"aria-label-default-editor": "Výchozí editor (kód nebo nástroj pro tvorbu)",
"aria-label-prom-type-type": "{{promType}} typ",
"aria-label-prometheus-type": "Typ Prometheus",
"aria-label-select-http-method": "Vyberte metodu HTTP",
"editor-options": {
"label-builder": "Nástroj pro tvorbu",
"label-code": "Kód"
},
"label-cache-level": "Úroveň vyrovnávací paměti",
"label-custom-query-parameters": "Vlastní parametry dotazu",
"label-default-editor": "Výchozí editor",
"label-disable-metrics-lookup": "Zakázat vyhledávání metrik",
"label-disable-recording-rules-beta": "Zakázat pravidla nahrávání (beta)",
"label-http-method": "Metoda HTTP",
"label-incremental-querying-beta": "Přírůstkové dotazování (beta)",
"label-prom-type-version": "{{promType}} verze",
"label-prometheus-type": "Typ Prometheus",
"label-query-overlap-window": "Okno překrytí dotazu",
"label-query-timeout": "Časový limit dotazu",
"label-scrape-interval": "Interval scrapingu",
"label-series-limit": "Limit řady",
"label-use-series-endpoint": "Použít koncový bod řady",
"more-info": "Další informace o konfiguraci typu a verze Prometheus ve zdrojích dat najdete v <2>dokumentaci k zajišťování</2>.",
"placeholder-example-maxsourceresolutionmtimeout": "Příklad: {{example}}",
"title-interval-behaviour": "Chování intervalu",
"title-other": "Ostatní",
"title-performance": "Výkon",
"title-query-editor": "Editor dotazů",
"tooltip-cache-level": "Nastaví úroveň ukládání do vyrovnávací paměti prohlížeče pro dotazy editoru. Pro zdroje dat s vysokou kardinalitou se doporučuje nastavit vyšší úroveň vyrovnávací paměti.",
"tooltip-custom-query-parameters": "Přidejte vlastní parametry do adresy URL dotazu Prometheus. Například {{example1}}, {{example2}}, {{example3}} nebo {{example4}}. Více parametrů by mělo být pospojováno pomocí {{concatenationChar}}.",
"tooltip-default-editor": "Nastavit výchozí možnost editoru pro všechny uživatele tohoto zdroje dat.",
"tooltip-disable-metrics-lookup": "Zaškrtnutím této možnosti zakážete výběr metrik a podporu metrik/štítků v automatickém doplňování pole dotazu. Užitečné, pokud máte problémy s výkonem u větších instancí Prometheus. ",
"tooltip-disable-recording-rules-beta": "Tato funkce zakáže pravidla nahrávání. Zapněte tuto možnost pro zlepšení výkonu nástěnky",
"tooltip-http-method": "K dotazu na zdroj dat Prometheus můžete použít metodu POST nebo GET HTTP. Doporučenou metodou je POST, protože umožňuje větší dotazy. Změňte na GET, pokud máte verzi Prometheus starší než 2.1 nebo pokud jsou požadavky POST ve vaší síti omezeny.",
"tooltip-incremental-querying-beta": "Tato funkce změní výchozí chování relativních dotazů tak, aby vždy požadovaly aktuální data z instance Prometheus. Místo toho budou výsledky dotazů uloženy do vyrovnávací paměti a budou požadovány pouze nové záznamy. Povolte tuto možnost pro snížení zatížení databáze a sítě.",
"tooltip-prom-type-version": "Tímto nastavíte verzi své instance {{promType}}, pokud není automaticky nakonfigurována.",
"tooltip-prometheus-type": "Nastavte hodnotu na typ databáze Prometheus, např. Prometheus, Cortex, Mimir nebo Thanos. Změna tohoto pole uloží aktuální nastavení. Některé typy Prometheus podporují nebo nepodporují různá rozhraní API. Některé typy například podporují shodu regulérního výrazu pro dotazy na štítky za účelem zlepšení výkonu. Některé typy mají API pro metadata. Pokud toto nastavíte nesprávně, při dotazování na metriky a štítky můžete zaznamenat neobvyklé chování. Zkontrolujte dokumentaci Prometheus a ujistěte se, že zadáváte správný typ.",
"tooltip-query-overlap-window": "Nastavte dobu trvání, například {{example1}} nebo {{example2}} nebo {{example3}}. Výchozí hodnota: {{default}}. Tato doba trvání bude přidána k době trvání každého přírůstkového požadavku.",
"tooltip-query-timeout": "Nastavte časový limit dotazu Prometheus.",
"tooltip-scrape-interval": "Tento interval udává, jak často Prometheus shromažďuje cíle. Nastavte hodnotu na typický interval shromažďování a vyhodnocování nakonfigurovaný v konfiguračním souboru Prometheus. Pokud nastavíte hodnotu vyšší než interval konfiguračního souboru Prometheus, Grafana vyhodnotí data podle tohoto intervalu a uvidíte méně datových bodů. Výchozí nastavení je {{default}}.",
"tooltip-series-limit": "Limit se vztahuje na všechny zdroje (metriky, štítky a hodnoty) pro oba koncové body (řady a štítky). Ponechte pole prázdné pro použití výchozího limitu (40 000). Nastavením na 0 zakážete limit a načtete vše  může to způsobit problémy s výkonem. Výchozí limit je 40 000.",
"tooltip-use-series-endpoint": "Zaškrtnutím této možnosti upřednostníte koncový bod řady s parametrem {{exampleParameter}} před koncovým bodem hodnot štítků s parametrem {{exampleParameter}}. Zatímco koncový bod hodnot štítků je považován za výkonnější, někteří uživatelé mohou upřednostňovat řady kvůli metodě POST, zatímco koncový bod hodnot štítků má pouze metodu GET."
}
},
"metrics-browser": {
"disabled-label": "(zakázáno)",
"enabled-label": "Prohlížeč metrik"
},
"prom-query-legend-editor": {
"get-legend-mode-options": {
"description-auto": "Zahrnuje pouze jedinečné štítky",
"description-custom": "Zadejte šablonu pro pojmenování",
"description-verbose": "Všechny názvy a hodnoty štítků",
"label-auto": "Auto.",
"label-custom": "Vlastní",
"label-verbose": "Podrobný"
}
},
"promql": {
"getAggregationOptions": {
"documentation-avg": "Vypočítat průměr z rozměrů",
"documentation-bottomk": "Nejmenší k prvky podle hodnoty vzorku",
"documentation-count": "Spočítat prvky ve vektoru",
"documentation-count-values": "Spočítat počet prvků se stejnou hodnotou",
"documentation-group": "Všechny hodnoty ve výsledném vektoru jsou 1",
"documentation-max": "Vybrat maximum nad rozměry",
"documentation-min": "Vybrat minimum podle rozměrů",
"documentation-quantile": "Vypočítat φ-kvantil (0 ≤ φ ≤ 1) podle rozměrů",
"documentation-stddev": "Vypočítat populační směrodatnou odchylku podle rozměrů",
"documentation-stdvar": "Vypočítat standardní rozptyl populace podle rozměrů",
"documentation-sum": "Vypočítat součet podle rozměrů",
"documentation-topk": "Největší k prvky podle hodnoty vzorku"
},
"getFunctions": {
"documentation-abs": "Vrátí vstupní vektor se všemi hodnotami vzorku převedenými na jejich absolutní hodnotu.",
"documentation-absent": "Vrátí prázdný vektor, pokud vektor, který mu byl předán, obsahuje nějaké prvky, a vektor s jedním prvkem s hodnotou 1, pokud vektor, který mu byl předán, neobsahuje žádné prvky. To je užitečné pro upozorňování, když pro danou kombinaci názvu metriky a štítku neexistuje žádná časová řada.",
"documentation-absent-over-time": "Vrátí prázdný vektor, pokud vektor rozsahu, který mu byl předán, obsahuje nějaké prvky, a vektor s jedním prvkem s hodnotou 1, pokud vektor rozsahu, který mu byl předán, neobsahuje žádné prvky.",
"documentation-avg-over-time": "Průměrná hodnota všech bodů v zadaném intervalu.",
"documentation-ceil": "Zaokrouhlí vzorové hodnoty všech prvků v části „v“ nahoru na nejbližší celé číslo.",
"documentation-changes": "Pro každou vstupní časovou řadu vrátí „changes(v range-vector)“ počet změn její hodnoty v zadaném časovém rozsahu jako okamžitý vektor.",
"documentation-clamp": "Omezí hodnoty všech prvků v části „v“ tak, aby měly dolní hranici „min“ a horní hranici „max“.",
"documentation-clamp-max": "Omezí hodnoty všech prvků v části „v“ tak, aby měly horní hranici „max“.",
"documentation-clamp-min": "Omezí hodnoty všech prvků v části „v“ tak, aby měly dolní hranici „min“.",
"documentation-count-over-time": "Počet všech hodnot v zadaném intervalu.",
"documentation-count-scalar": "Vrací počet prvků vektoru časové řady jako skalární hodnotu. To je rozdíl od agregačního operátoru „count()“, který vždy vrací vektor (prázdný, pokud je vstupní vektor prázdný) a umožňuje seskupování podle štítků pomocí klauzule „by“.",
"documentation-day-of-month": "Vrátí den v měsíci pro každý z daných časů v UTC. Vrácené hodnoty jsou od 1 do 31.",
"documentation-day-of-week": "Vrátí den v týdnu pro každý z daných časů v UTC. Vrácené hodnoty jsou od 0 do 6, kde 0 znamená neděli atd.",
"documentation-day-of-year": "Vrátí den v roce pro každý z daných časů v UTC. Vrácené hodnoty jsou od 1 do 365 pro roky, které nejsou přestupné, a od 1 do 366 pro přestupné roky.",
"documentation-days-in-month": "Vrátí počet dní v měsíci pro každý z daných časů v UTC. Vrácené hodnoty jsou od 28 do 31.",
"documentation-deg": "Převádí radiány na stupně pro všechny prvky v",
"documentation-delta": "Vypočítá rozdíl mezi první a poslední hodnotou každého prvku časové řady ve vektoru rozsahu „v“ a vrátí okamžitý vektor s danými deltami a ekvivalentními štítky. Delta je extrapolována tak, aby pokrývala celý časový rozsah, jak je specifikováno ve voliči vektoru rozsahu, takže je možné získat neceločíselný výsledek, i když jsou všechny hodnoty vzorku celá čísla.",
"documentation-deriv": "Vypočítá derivaci časové řady za sekundu ve vektoru rozsahu „v“ pomocí jednoduché lineární regrese.",
"documentation-double-exponential-smoothing": "Vytvoří vyhlazenou hodnotu pro časové řady na základě rozsahu v části „v“. Čím nižší je vyhlazovací faktor „sf“, tím větší důležitost je přikládána starým datům. Čím vyšší je trendový faktor „tf“, tím více trendů v datech je zvažováno. „sf“ i „tf“ musí být mezi 0 a 1.",
"documentation-drop-common-labels": "Vynechá všechny štítky, které mají stejný název a hodnotu ve všech řadách ve vstupním vektoru.",
"documentation-exp": "Vypočítá exponenciální funkci pro všechny prvky v části „v“.\nZvláštní případy jsou:\n* „Exp(+Inf) = +Inf“ \n* „Exp(NaN) = NaN“",
"documentation-floor": "Zaokrouhlí vzorové hodnoty všech prvků v části „v“ dolů na nejbližší celé číslo.",
"documentation-histogram-avg": "Vrátí aritmetický průměr pozorovaných hodnot uložených v nativním histogramu. Vzorky, které nejsou nativními histogramy, jsou ignorovány a ve vráceném vektoru se nezobrazují.",
"documentation-histogram-count": "Vrátí počet pozorování uložených v nativním histogramu.",
"documentation-histogram-fraction": "Vrátí odhadovaný zlomek pozorování mezi zadanou dolní a horní hodnotou.",
"documentation-histogram-quantile": "Vypočítá φ-kvantil (0 ≤ φ ≤ 1) ze segmentů „b“ histogramu. Vzorky v „b“ jsou počty pozorování v každém segmentu. Každý vzorek musí mít štítek „le“, kde hodnota štítku označuje inkluzivní horní hranici segmentu. (Vzorky bez takového štítku jsou tiše ignorovány.) Typ metriky histogramu automaticky poskytuje časové řady s příponou „_bucket“ a příslušnými štítky.",
"documentation-histogram-stddev": "Vrátí odhadovanou směrodatnou odchylku pozorování v nativním histogramu na základě geometrického průměru segmentů, kde se pozorování nacházejí.",
"documentation-histogram-stdvar": "Vrátí odhadovaný standardní rozptyl pozorování v nativním histogramu.",
"documentation-histogram-sum": "Vrátí součet pozorování uložených v nativním histogramu.",
"documentation-holt-winters": "Přejmenováno na double_exponential_smoothing v prometheus v3.x. Pro verze Prometheus rovné a větší než v3.0 použijte double_exponential_smoothing. \n\nVytváří vyhlazenou hodnotu pro časové řady na základě rozsahu v „v“. Čím nižší je vyhlazovací faktor „sf“, tím větší význam je přikládán starým datům. Čím vyšší je trendový faktor „tf“, tím více trendů v datech je zvažováno. „sf“ i „tf“ musí být mezi 0 a 1.",
"documentation-hour": "Vrátí hodinu dne pro každý z daných časů v UTC. Vrácené hodnoty jsou od 0 do 23.",
"documentation-idelta": "Vypočítá rozdíl mezi posledními dvěma vzorky ve vektoru rozsahu „v“ a vrátí okamžitý vektor s danými deltami a ekvivalentními štítky.",
"documentation-increase": "Vypočítá nárůst časové řady ve vektoru rozsahu. Přerušení monotónnosti (například resetování počítadla v důsledku restartů cíle) se automaticky upravují. Nárůst je extrapolován tak, aby pokrýval celý časový rozsah, jak je specifikováno ve voliči vektoru rozsahu, takže je možné získat neceločíselný výsledek, i když se počítadlo zvyšuje pouze o celočíselné přírůstky.",
"documentation-info": "Vrátí nejnovější podrobnosti a metadata o skupině metrik, jako jsou jejich štítky a aktuální hodnoty, bez provádění jakýchkoli výpočtů",
"documentation-irate": "Vypočítá okamžitou rychlost nárůstu časové řady ve vektoru rozsahu za sekundu. Vychází z posledních dvou datových bodů. Přerušení monotónnosti (například resetování počítadla v důsledku restartu cíle) se automaticky upravují.",
"documentation-label-join": "Pro každou časovou řadu v části „v“ spojí všechny hodnoty všech „src_labels“ pomocí „separator“ a vrátí časové řady se štítkem „dst_label“ obsahujícím spojenou hodnotu. V této funkci může být libovolný počet „src_labels“.",
"documentation-label-replace": "Pro každou časovou řadu v části „v“ porovná „label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)“ regulární výraz „regulérní výraz“ se štítkem „src_label“. Pokud se shoduje, pak se vrátí časová řada se štítkem „dst_label“ nahrazeným rozšířením „replacement“. „$1“ se nahradí první odpovídající podskupinou, „$2“ druhou atd. Pokud se regulární výraz neshoduje, pak se časová řada vrátí beze změny.",
"documentation-last-over-time": "Nejnovější bodová hodnota v zadaném intervalu.",
"documentation-ln": "Vypočítá přirozený logaritmus pro všechny prvky v části „v“.\nZvláštní případy jsou:\n * „ln(+Inf) = +Inf“\n * „ln(0) = -Inf“\n * „ln(x < 0) = NaN“\n * „ln(NaN) = NaN“",
"documentation-log10": "Vypočítá desetinný logaritmus pro všechny prvky v části „v“. Zvláštní případy jsou ekvivalentní těm v „ln“.",
"documentation-log2": "Vypočítá binární logaritmus pro všechny prvky v části „v“. Zvláštní případy jsou ekvivalentní těm v části „ln“.",
"documentation-max-over-time": "Maximální hodnota všech bodů v zadaném intervalu.",
"documentation-min-over-time": "Minimální hodnota všech bodů v zadaném intervalu.",
"documentation-minute": "Vrátí minutu hodiny pro každý z daných časů v UTC. Vrácené hodnoty jsou od 0 do 59.",
"documentation-month": "Vrátí měsíc v roce pro každý z daných časů v UTC. Vrácené hodnoty jsou od 1 do 12, kde 1 znamená leden atd.",
"documentation-pi": "Vrátí pí",
"documentation-predict-linear": "Předpovídá hodnotu časové řady „t“ sekund od této chvíle na základě vektoru rozsahu „v“ pomocí jednoduché lineární regrese.",
"documentation-present-over-time": "Hodnota 1 pro libovolnou řadu v zadaném intervalu.",
"documentation-quantile-over-time": "φ-kvantil (0 ≤ φ ≤ 1) hodnot v zadaném intervalu.",
"documentation-rad": "Převádí stupně na radiány pro všechny prvky v části v",
"documentation-rate": "Vypočítá průměrnou rychlost nárůstu časové řady za sekundu ve vektoru rozsahu. Přerušení monotónnosti (například resetování počítadla v důsledku restartů cíle) se automaticky upravují. Výpočet také extrapoluje na konce časového rozsahu, což umožňuje vynechané metriky nebo nedokonalé srovnání cyklů metrik s časovým obdobím rozsahu.",
"documentation-resets": "Pro každou vstupní časovou řadu vrátí „resets(v range-vector)“ počet resetů počítadla v zadaném časovém rozsahu jako okamžitý vektor. Jakékoli snížení hodnoty mezi dvěma po sobě jdoucími vzorky je interpretováno jako reset počítadla.",
"documentation-round": "Zaokrouhlí vzorové hodnoty všech prvků v části „v“ na nejbližší celé číslo. Shody se zaokrouhlují nahoru. Volitelný argument „to_nearest“ umožňuje určit nejbližší násobek, na který by měly být vzorové hodnoty zaokrouhleny. Tento násobek může být také zlomkem.",
"documentation-scalar": "Vzhledem k jednoprvkovému vstupnímu vektoru vrátí „scalar(v instant-vector)“ hodnotu vzorku tohoto jednoho prvku jako skalární hodnotu. Pokud vstupní vektor nemá přesně jeden prvek, „scalar“ vrátí „NaN“.",
"documentation-sgn": "Vrátí vektor se všemi hodnotami vzorku převedenými na jejich znaménko, definované takto: 1, pokud je v kladné, -1, pokud je v záporné, a 0, pokud se v rovná nule.",
"documentation-sort": "Vrátí vektorové prvky seřazené podle jejich vzorových hodnot ve vzestupném pořadí.",
"documentation-sort-desc": "Vrátí vektorové prvky seřazené podle jejich vzorových hodnot v sestupném pořadí.",
"documentation-sqrt": "Vypočítá druhou odmocninu všech prvků v části „v“.",
"documentation-stddev-over-time": "Standardní odchylka hodnot v zadaném intervalu.",
"documentation-stdvar-over-time": "Standardní rozptyl hodnot v zadaném intervalu.",
"documentation-sum-over-time": "Součet všech hodnot v zadaném intervalu.",
"documentation-time": "Vrátí počet sekund od 1. ledna 1970 UTC. Upozorňujeme, že se nejedná o aktuální čas, ale o čas, kdy má být výraz vyhodnocen.",
"documentation-timestamp": "Vrátí časové razítko každého ze vzorků daného vektoru jako počet sekund od 1. ledna 1970 UTC.",
"documentation-vector": "Vrátí skalární hodnotu jako vektor bez štítků.",
"documentation-year": "Vrátí rok pro každý z daných časů v UTC."
},
"getTrigonometricFunctions": {
"documentation-acos": "vypočítá arkus kosinus všech prvků v {{argument}}",
"documentation-acosh": "vypočítá inverzní hyperbolický kosinus všech prvků v {{argument}}",
"documentation-asin": "vypočítá arkus sinus všech prvků v {{argument}}",
"documentation-asinh": "vypočítá inverzní hyperbolický sinus všech prvků v {{argument}}",
"documentation-atan": "vypočítá arkus tangens všech prvků v {{argument}}",
"documentation-atanh": "vypočítá inverzní hyperbolický tangens všech prvků v {{argument}}",
"documentation-cos": "vypočítá kosinus všech prvků v {{argument}}",
"documentation-cosh": "vypočítá hyperbolický kosinus všech prvků v {{argument}}",
"documentation-sin": "vypočítá sinus všech prvků v {{argument}}",
"documentation-sinh": "vypočítá hyperbolický sinus všech prvků v {{argument}}",
"documentation-tan": "vypočítá tangens všech prvků v {{argument}}",
"documentation-tanh": "vypočítá hyperbolický tangens všech prvků v {{argument}}"
}
},
"querybuilder": {
"feedback-link": {
"give-feedback": "Poskytnout zpětnou vazbu",
"title-give-feedback": "Průzkumník metrik je nový. Dejte nám prosím vědět, jak ho můžeme vylepšit"
},
"get-collapsed-info": {
"exemplars": "Příklady: {{value}}",
"format": "Formát: {{value}}",
"legend": "Legenda: {{value}}",
"step": "Krok: {{value}}",
"type": "Typ: {{value}}"
},
"get-placeholders": {
"browse": "Hledat metriky podle názvu",
"type": "Filtrovat podle typu"
},
"get-prom-types": {
"description-counter": "Kumulativní metrika, která představuje jedno monotónně rostoucí počítadlo, jehož hodnota se může při restartu pouze zvýšit nebo vynulovat.",
"description-gauge": "Metrika, která představuje jednu číselnou hodnotu, která může libovolně stoupat a klesat.",
"description-histogram": "Histogram vzorkuje pozorování (obvykle údaje jako doba trvání požadavků nebo velikosti odpovědí) a počítá je v konfigurovatelných segmentech.",
"description-native-histogram": "Nativní histogramy se od klasických histogramů Prometheus liší v několika ohledech: Hranice nativních histogramových segmentů se počítají podle vzorce, který závisí na měřítku (rozlišení) nativního histogramu, a nejsou definovány uživatelem.",
"description-no-type": "Tyto metriky nemají v metadatech definovaný typ.",
"description-summary": "Shrnuje vzorky pozorování (obvykle údaje jako délka požadavků a velikost odpovědí) a dokáže vypočítat konfigurovatelné kvantily v posuvném časovém okně.",
"description-unknown": "Těmto metrikám byl v metadatech přiřazen neznámý typ.",
"label-counter": "Počítadlo",
"label-gauge": "Měřidlo",
"label-histogram": "Histogram",
"label-native-histogram": "Nativní histogram",
"label-no-type": "Žádný typ",
"label-summary": "Shrnutí",
"label-unknown": "Neznámé"
},
"handle-function": {
"text": {
"query-parsing-is-ambiguous": "Analýza dotazu je nejednoznačná."
}
},
"label-filter-item": {
"aria-label-remove": "Odebrat {{name}}",
"placeholder-select-label": "Vybrat štítek",
"placeholder-select-value": "Vyberte hodnotu"
},
"label-filters": {
"label-filters": "Filtry štítků",
"label-label-filters": "Filtry štítků",
"tooltip-label-filters": "Volitelné: používá se k filtrování metriky vybrané pro tento typ dotazu."
},
"label-param-editor": {
"loadingMessage-loading-labels": "Načítání štítků",
"noOptionsMessage-no-labels-found": "Nebyly nalezeny žádné štítky"
},
"metric-combobox": {
"async-select": {
"aria-label-open-metrics-explorer": "Otevřít průzkumníka metrik",
"placeholder-select-metric": "Vybrat metriku",
"tooltip-open-metrics-explorer": "Otevřít průzkumníka metrik"
},
"label-metric": "Metrika",
"tooltip-metric": "Volitelné: vrátí seznam hodnot štítků pro název štítku v zadané metrice."
},
"metrics-modal": {
"aria-label-browse-metrics": "Procházet metriky",
"currently-selected": "Aktuálně vybráno: {{selected}}",
"metrics-pre-filtered": "Tyto metriky byly předem filtrovány štítky vybranými ve filtrech štítků.",
"title-metrics-explorer": "Průzkumník metrik"
},
"nested-query": {
"label": {
"ignoring": "Ignorování",
"on": "Zapnuto"
},
"operator": "Operátor",
"tooltip-remove-match": "Odebrat shodu",
"vector-matches": "Vektorové shody"
},
"operation-editor": {
"not-found": "Operace {{id}} nebyla nalezena",
"title-remove": "Odebrat {{name}}"
},
"operation-header": {
"placeholder-replace-with": "Nahradit",
"title-click-to-view-alternative-operations": "Kliknutím zobrazíte alternativní operace",
"title-remove-operation": "Odebrat operaci"
},
"operation-info-button": {
"title-click-to-show-description": "Kliknutím zobrazíte popis",
"title-remove-operation": "Odebrat operaci"
},
"operation-list": {
"operations": "Operace",
"placeholder-search": "Hledat",
"title-add-operation": "Přidat operaci"
},
"operation-param-editor": {
"title-add": "Přidat {{name}}",
"title-remove": "Odebrat {{name}}"
},
"operation-utils": {
"getAggregationExplainer": {
"label-by": "Vypočítá {{aggregationName}} podle rozměrů při zachování {{labelWord}} {{labels}}.",
"label-default": "Vypočítá {{aggregationName}} podle rozměrů.",
"label-without": "Vypočítá {{aggregationName}} podle rozměrů {{labels}}. Všechny ostatní štítky jsou zachovány."
}
},
"prom-query-builder-options": {
"aria-label-exemplars": "Přepínač příkladů.",
"aria-label-format": "Formátovat kombinované pole",
"aria-label-lower-limit-parameter": "Textové pole „Min. krok“, nastaví dolní limit pro parametr kroku",
"aria-label-select-resolution": "Vyberte rozlišení",
"aria-label-type": "Skupina přepínačů typu rádio",
"format-options": {
"label-heatmap": "Teplotní mapa",
"label-table": "Tabulka",
"label-time-series": "Časové řady"
},
"label-exemplars": "Příklady",
"label-format": "Formát",
"label-min-step": "Min. krok",
"label-resolution": "Rozlišení",
"label-type": "Typ",
"placeholder-auto": "auto.",
"title-options": "Možnosti",
"tooltip-min-step": "Dodatečný dolní limit pro parametr kroku dotazu Prometheus a pro proměnné <2>{{interval}}</2> a <4>{{rateInterval}}</4>."
},
"prom-query-editor-selector": {
"body-syntax-error": "Došlo k syntaktické chybě nebo nelze vizualizovat strukturu dotazu při přepnutí do režimu nástroje pro tvorbu. Části dotazu mohou být ztraceny.",
"confirmText-continue": "Pokračovat",
"kick-start-your-query": "Spustit dotaz",
"label-explain": "Vysvětlit",
"run-queries": "Spustit dotazy",
"title-parsing-error-switch-builder": "Chyba analýzy: Přepnout do režimu nástroje pro tvorbu?"
},
"prom-query-legend-editor": {
"aria-label-legend": "Kombinované pole legendy",
"label-legend": "Legenda",
"placeholder-select-legend-mode": "Vyberte režim legendy",
"tooltip-legend": "Přepsání názvu řady nebo šablony. Např. {{templateExample}} bude nahrazeno hodnotou štítku pro {{labelName}}."
},
"query-builder-hints": {
"hint-details": "nápověda: {{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "Nástroj pro tvorbu",
"label-code": "Kód"
}
},
"query-pattern": {
"apply-query": "Použít dotaz",
"aria-label-apply-query-starter-button": "tlačítko Použít spouštěč dotazu",
"aria-label-back-button": "tlačítko Zpět",
"aria-label-create-new-query-button": "tlačítko Vytvořit nový dotaz",
"aria-label-raw-query": "Nezpracovaný dotaz: {{patternName}}",
"aria-label-use-this-query-button": "použít toto tlačítko dotazu",
"back": "Zpět",
"create-new-query": "Vytvořit nový dotaz",
"use-this-query": "Použít tento dotaz"
},
"query-patterns-modal": {
"aria-label-close-kick-start-your-query-modal": "zavřít modální okno spuštění dotazu",
"aria-label-kick-start-your-query-modal": "Spustit modální okno dotazu",
"aria-label-toggle-query-starter": "otevřít a zavřít {{patternType}} kartu spouštěče dotazu",
"close": "Zavřít",
"description-kick-start-your-query": "Spusťte dotaz výběrem jednoho z těchto dotazů. Poté můžete pokračovat v dokončení dotazu.",
"label-toggle-query-starter": "Spouštěče dotazů: {{patternType}}",
"title-kick-start-your-query": "Spustit dotaz"
},
"raw-query": {
"aria-label-selector": "volič"
},
"results-table": {
"content-descriptive-type": "Při vytváření {{descriptiveType}} Prometheus zobrazí více řad s počítadlem typů. ",
"description": "Popis",
"message-expand-label-filters": "Nebyly nalezeny žádné metriky. Zkuste rozšířit filtry štítků.",
"message-expand-search": "Nebyly nalezeny žádné metriky. Zkuste rozšířit vyhledávání a filtry.",
"message-no-metrics-found": "Ve zdroji dat nebyly nalezeny žádné metriky.",
"name": "Název",
"type": "Typ"
}
}
}
}

View File

@@ -1,492 +0,0 @@
{
"grafana-prometheus": {
"components": {
"annotation-query-editor": {
"annotation-data-load-error": "Fehler beim Laden der Anmerkungsdaten!",
"aria-label-lower-limit-parameter": "Untergrenze für den Schrittparameter festlegen",
"label-min-step": "Min. Schritt",
"label-series-value-as-timestamp": "Reihenwert als Zeitstempel",
"label-tags": "Tags",
"label-text": "Text",
"label-title": "Titel",
"placeholder-auto": "Auto",
"tooltip-either-pattern-example-instance-replaced-label": "Verwenden Sie entweder den Namen oder ein Muster. Ein Beispiel: {{labelTemplate}} wird durch den Label-Wert für das Label {{labelName}} ersetzt.",
"tooltip-min-step": "Eine zusätzliche Untergrenze für den Schrittparameter der Prometheus-Abfrage und für die Variablen <2>{{intervalVar}}</2> und <4>{{rateIntervalVar}}</4>.",
"tooltip-timestamp-milliseconds-series-value-seconds-multiply": "Die Einheit des Zeitstempels ist Millisekunden. Wenn die Einheit des Reihenwerts Sekunden ist, multiplizieren Sie den Bereichsvektor mit 1000."
},
"get-query-type-options": {
"description": {
"instant-query-range": "Eine Instant-Abfrage und eine Bereichsabfrage ausführen"
},
"label": {
"both": "Beides"
},
"range-options": {
"description": {
"query-range": "Abfrage über einen bestimmten Zeitraum ausführen"
},
"label": {
"instant": "Instant",
"range": "Bereich"
}
}
},
"label-selector": {
"aria-label-filter-expression-for-label": "Filterausdruck für Label",
"description-select-labels": "Sobald die Label-Werte ausgewählt sind, werden nur mögliche Label-Kombinationen angezeigt.",
"select-labels-to-search-in": "2. Wählen Sie Labels aus, in denen gesucht werden soll"
},
"metric-selector": {
"aria-label-filter-expression-for-metric": "Filterausdruck für Metrik",
"aria-label-limit-results-from-series-endpoint": "Ergebnisse vom Reihenendpunkt begrenzen",
"description-series-limit": "Die Grenze gilt für alle Metriken, Labels und Werte. Wenn Sie das Feld leer lassen, wird die Standardgrenze verwendet. Durch eine Einstellung auf 0 wird die Grenze deaktiviert und alles abgerufen  dies kann zu Leistungsproblemen führen.",
"label-select-metric": "Sobald eine Metrik ausgewählt ist, werden nur mögliche Labels angezeigt. Labels werden durch die folgende Reihengrenze begrenzt.",
"select-a-metric": "1. Metrik auswählen",
"series-limit": "Reihengrenze"
},
"prom-cheat-sheet": {
"prom-ql-cheat-sheet": "PromQL Cheat Sheet"
},
"prom-exemplar-field": {
"exemplars": "Exemplare",
"tooltip-disable-query": "Abfrage mit Exemplaren deaktivieren",
"tooltip-enable-query": "Abfrage mit Exemplaren aktivieren"
},
"prom-explore-extra-field": {
"aria-label-prometheus-extra-field": "Zusätzliches Feld in Prometheus",
"aria-label-query-type-field": "Abfragetyp-Feld",
"aria-label-step-field": "Schrittfeld",
"min-step": "Min. Schritt",
"query-type": "Abfragetyp",
"tooltip-units-builtin-variables-example-interval-rateinterval": "Hier können Zeiteinheiten und integrierte Variablen verwendet werden, zum Beispiel: {{example1}}, {{example2}}, {{example3}}, {{example4}}, {{example5}}, {{example6}}, {{example7}} (Standardwert bei fehlender Einheit: {{default}})"
},
"prom-query-field": {
"placeholder-enter-a-prom-ql-query": "Geben Sie eine PromQL-Abfrage ein …"
},
"prom-variable-query-editor": {
"aria-label-classic-query": "Klassische Abfrage",
"aria-label-metric-regex": "Metrik-Regex",
"aria-label-metric-selector": "Metrik-Selektor",
"aria-label-prometheus-query": "Prometheus-Abfrage",
"aria-label-query-type": "Abfragetyp",
"aria-label-series-query": "Reihenabfrage",
"label-classic-query": "Klassische Abfrage",
"label-label": "Label",
"label-metric-regex": "Metrik-RegEx",
"label-query": "Abfrage",
"label-query-type": "Abfragetyp",
"label-series-query": "Reihenabfrage",
"placeholder-classic-query": "Klassische Abfrage",
"placeholder-metric-regex": "Metrik-Regex",
"placeholder-prometheus-query": "Prometheus-Abfrage",
"placeholder-select-query-type": "Abfragetyp auswählen",
"placeholder-series-query": "Reihenabfrage",
"returns-metrics-matching-specified-metric-regex": "Gibt eine Liste von Metriken zurück, die mit dem angegebenen Metrik-Regex übereinstimmen.",
"tooltip-classic-query": "Die ursprüngliche Implementierung des Prometheus-Variablen-Abfrageeditors. Geben Sie eine Zeichenfolge mit dem richtigen Abfragetyp und den richtigen Parametern ein, wie in diesen Dokumenten beschrieben. Zum Beispiel: {{exampleQuery}}.",
"tooltip-label": "Gibt eine Liste von Label-Werten für den Label-Namen in allen Metriken zurück, es sei denn, die Metrik ist angegeben.",
"tooltip-metric-regex": "Gibt eine Liste von Label-Namen zurück, die optional gemäß dem angegebenen Metrik-Regex gefiltert werden.",
"tooltip-query": "Gibt eine Liste der Prometheus-Abfrageergebnisse für die Abfrage zurück. Dies kann Prometheus-Funktionen umfassen, z. B. {{exampleQuery}}.",
"tooltip-query-type": "Das Prometheus-Datenquellen-Plugin bietet die folgenden Abfragetypen für Vorlagenvariablen.",
"tooltip-series-query": "Geben Sie eine Metrik mit Labels ein, nur eine Metrik oder nur Labels, z. B. {{example1}}, {{example2}} oder {{example3}}. Gibt eine Liste der Zeitreihen zurück, die den eingegebenen Daten zugeordnet sind."
},
"selector-actions": {
"aria-label-selector": "Selektor",
"aria-label-selector-clear-button": "Löschen-Schaltfläche des Selektors",
"aria-label-use-selector-as-metrics-button": "Schaltfläche zur Verwendung des Selektors als Metrik",
"aria-label-use-selector-for-query-button": "Schaltfläche zur Verwendung des Selektors für die Abfrage",
"aria-label-validate-submit-button": "Validieren und Absenden-Schaltfläche",
"clear": "Löschen",
"resulting-selector": "4. Resultierender Selektor",
"use-as-rate-query": "Als Bewertungsabfrage verwenden",
"use-query": "Abfrage verwenden",
"validate-selector": "Selektor validieren"
},
"value-selector": {
"aria-label-filter-expression-for-label-values": "Filterausdruck für Label-Werte",
"aria-label-values-for": "Werte für {{labelKey}}",
"description-search-field-values-across-selected-labels": "Verwenden Sie das Suchfeld, um Werte über mehrere ausgewählte Labels zu finden.",
"select-multiple-values-for-your-labels": "3. Wählen Sie (mehrere) Werte für Ihre Labels aus"
}
},
"configuration": {
"alerting-settings-overhaul": {
"label-allow-as-recording-rules-target": "Als Ziel für Aufnahmeregeln zulassen",
"label-manage-alerts-via-alerting-ui": "Benachrichtigungen über Alerting-UI verwalten",
"title-alerting": "Meldungen",
"tooltip-allow-as-recording-rules-target": "Erlauben, dass diese Datenquelle als Ziel für das Schreiben von Aufnahmeregeln ausgewählt wird.",
"tooltip-manage-alerts-via-alerting-ui": "Verwalten Sie Warnregeln für diese Datenquelle. Fügen Sie eine Altermanager-Datenquelle hinzu, um andere Warnressourcen zu verwalten."
},
"config-editor": {
"browser-access-mode-error": "Der Browserzugriffsmodus in der Prometheus-Datenquelle ist nicht mehr verfügbar. Wechseln Sie in den Serverzugriffsmodus.",
"description-advanced-settings": "Zusätzliche Einstellungen sind optionale Einstellungen, die für mehr Kontrolle über Ihre Datenquelle konfiguriert werden können.",
"title-advanced-settings": "Erweiterte Einstellungen",
"title-error": "Fehler"
},
"data-source-http-settings-overhaul": {
"tooltip-browser-access-mode": "Ihre Zugriffsmethode ist <1>Browser</1>, d. h. die URL muss über den Browser zugänglich sein.",
"tooltip-http-url": "Geben Sie eine vollständige HTTP-URL an (z. B. {{exampleURL}})",
"tooltip-server-access-mode": "Ihre Zugriffsmethode ist <1>Server</1>, d. h. die URL muss über das Grafana-Backend/den Grafana-Server zugänglich sein."
},
"docs-tip": {
"visit-docs-for-more-details-here": "Weitere Details finden Sie hier in der Dokumentation."
},
"exemplar-setting": {
"label-data-source": "Datenquelle",
"label-internal-link": "Interner Link",
"label-label-name": "Label-Name",
"label-remove-exemplar-link": "Exemplar-Link entfernen",
"label-url": "URL",
"label-url-label": "URL-Label",
"placeholder-go-to-examplecom": "Zu example.com",
"placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}",
"placeholder-trace-id": "traceID",
"title-remove-exemplar-link": "Exemplar-Link entfernen",
"tooltip-data-source": "Die Datenquelle, zu der das Exemplar navigiert.",
"tooltip-internal-link": "Aktivieren Sie diese Option, wenn Sie einen internen Link haben. Wenn dies aktiviert ist, wird der Datenquellen-Selektor angezeigt. Wählen Sie den Backend-Tracing-Datenspeicher für Ihre Exemplar-Daten aus.",
"tooltip-label-name": "Der Name des Felds im Label-Objekt, das für den Erhalt der TraceID verwendet werden soll.",
"tooltip-url": "Die URL des Trace-Backends, die der Nutzer zur Ansicht des Trace aufrufen würde",
"tooltip-url-label": "Damit können Sie das Schaltflächen-Label im Feld der Exemplar-TraceID überschreiben."
},
"exemplars-settings": {
"add": "Hinzufügen",
"no-exemplars-configurations": "Keine Exemplar-Konfigurationen",
"title-exemplars": "Exemplare"
},
"prom-settings": {
"aria-label-default-editor": "Standard-Editor (Code oder Builder)",
"aria-label-prom-type-type": "{{promType}} Typ",
"aria-label-prometheus-type": "Prometheus-Typ",
"aria-label-select-http-method": "HTTP-Methode auswählen",
"editor-options": {
"label-builder": "Builder",
"label-code": "Code"
},
"label-cache-level": "Cache-Level",
"label-custom-query-parameters": "Benutzerdefinierte Abfrageparameter",
"label-default-editor": "Standard-Editor",
"label-disable-metrics-lookup": "Metrik-Suche deaktivieren",
"label-disable-recording-rules-beta": "Aufnahmeregeln deaktivieren (Beta)",
"label-http-method": "HTTP-Methode",
"label-incremental-querying-beta": "Inkrementelle Abfrage (Beta)",
"label-prom-type-version": "{{promType}} Version",
"label-prometheus-type": "Prometheus-Typ",
"label-query-overlap-window": "Fenster der Abfrage-Überschneidung",
"label-query-timeout": "Abfrage-Timeout",
"label-scrape-interval": "Scrape-Intervall",
"label-series-limit": "Reihengrenze",
"label-use-series-endpoint": "Reihenendpunkt verwenden",
"more-info": "Weitere Informationen zur Konfiguration des Typs und der Version von Prometheus in Datenquellen finden Sie in der <2>Bereitstellungsdokumentation</2>.",
"placeholder-example-maxsourceresolutionmtimeout": "Beispiel: {{example}}",
"title-interval-behaviour": "Intervallverhalten",
"title-other": "Sonstiges",
"title-performance": "Leistung",
"title-query-editor": "Abfrage-Editor",
"tooltip-cache-level": "Legt die Browser-Cache-Level für Editor-Abfragen fest. Für Datenquellen mit hoher Kardinalität werden höhere Cache-Einstellungen empfohlen.",
"tooltip-custom-query-parameters": "Fügen Sie der URL der Prometheus-Abfrage benutzerdefinierte Parameter hinzu. Zum Beispiel {{example1}}, {{example2}}, {{example3}} oder {{example4}}. Mehrere Parameter sollten mit {{concatenationChar}} verknüpft werden.",
"tooltip-default-editor": "Legen Sie die Standard-Editoroption für alle Nutzer dieser Datenquelle fest.",
"tooltip-disable-metrics-lookup": "Wenn diese Option aktiviert ist, werden die Metrikauswahl und die Unterstützung für Metriken/Labels im Rahmen der Autovervollständigung des Abfragefelds deaktiviert. Dies hilft, wenn Sie Leistungsprobleme mit größeren Prometheus-Instanzen haben. ",
"tooltip-disable-recording-rules-beta": "Diese Funktion deaktiviert die Aufnahmeregeln. Wenn Sie dies aktivieren, wird die Dashboard-Leistung verbessert",
"tooltip-http-method": "Sie können entweder die HTTP-Methode POST oder GET für die Abfrage Ihrer Prometheus-Datenquelle verwenden. POST ist die empfohlene Methode, da sie größere Abfragen ermöglicht. Ändern Sie dies zu GET, wenn Ihre Prometheus-Version älter als 2.1 ist oder POST-Abfragen in Ihrem Netzwerk beschränkt sind.",
"tooltip-incremental-querying-beta": "Diese Funktion ändert das Standardverhalten von relativen Abfragen, sodass immer neue Daten von der Prometheus-Instanz abgefragt werden. Stattdessen werden Abfrageergebnisse zwischengespeichert und nur neue Datensätze abgefragt. Wenn Sie dies aktivieren, wird die Datenbank- und Netzwerklast reduziert.",
"tooltip-prom-type-version": "Nutzen Sie diese Option, um die Version Ihrer Instanz von {{promType}} festzulegen, wenn sie nicht automatisch konfiguriert wird.",
"tooltip-prometheus-type": "Stellen Sie dies auf den Typ Ihrer Prometheus-Datenbank ein, z. B. Prometheus, Cortex, Mimir oder Thanos. Wenn Sie dieses Feld ändern, werden Ihre aktuellen Einstellungen gespeichert. Bestimmte Typen von Prometheus unterstützen entweder verschiedene APIs oder nicht. Zum Beispiel unterstützen manche Typen Regex-Matching für Label-Abfragen, um die Leistung zu verbessern. Manche Typen haben eine API für Metadaten. Wenn Sie dies falsch einstellen, kann es bei der Abfrage von Metriken und Labels zu einem ungewöhnlichen Verhalten kommen. Bitte prüfen Sie Ihre Prometheus-Dokumentation, um sicherzugehen, dass Sie den richtigen Typ eingeben.",
"tooltip-query-overlap-window": "Stellen Sie eine Dauer ein, zum Beispiel {{example1}} oder {{example2}} oder {{example3}}. Standardwert: {{default}}. Diese Dauer wird zur Dauer jeder inkrementellen Abfrage hinzugefügt.",
"tooltip-query-timeout": "Legen Sie das Prometheus-Abfrage-Timeout fest.",
"tooltip-scrape-interval": "Dieses Intervall gibt an, wie oft Prometheus ein Scraping für Ziele durchführt. Stellen Sie dies auf das typische Scrape- und Bewertungsintervall ein, das in Ihrer Prometheus-Konfigurationsdatei konfiguriert ist. Wenn Sie dies auf einen größeren Wert als das Intervall Ihrer Prometheus-Konfigurationsdatei einstellen, wertet Grafana die Daten gemäß diesem Intervall aus und Sie sehen weniger Datenpunkte. Standardmäßig {{default}}.",
"tooltip-series-limit": "Die Grenze gilt für alle Ressourcen (Metriken, Labels und Werte) für beide Endpunkte (Reihen und Labels). Wenn Sie das Feld leer lassen, wird die Standardgrenze verwendet (40.000). Durch eine Einstellung auf 0 wird die Grenze deaktiviert und alles abgerufen  dies kann zu Leistungsproblemen führen. Die Standardgrenze beträgt 40.000.",
"tooltip-use-series-endpoint": "Wenn diese Option aktiviert ist, wird der Reihenendpunkt mit dem Parameter {{exampleParameter}} gegenüber dem Endpunkt der Label-Werte mit dem Parameter {{exampleParameter}} bevorzugt. Obwohl der Endpunkt der Label-Werte als leistungsfähiger gilt, könnten manche Nutzer die Reihe bevorzugen, da sie über eine POST-Methode verfügt, während der Endpunkt der Label-Werte nur über eine GET-Methode verfügt."
}
},
"metrics-browser": {
"disabled-label": "(Deaktiviert)",
"enabled-label": "Metriken-Browser"
},
"prom-query-legend-editor": {
"get-legend-mode-options": {
"description-auto": "Enthält nur eindeutige Labels",
"description-custom": "Namensvorlage bereitstellen",
"description-verbose": "Alle Label-Namen und -Werte",
"label-auto": "Auto",
"label-custom": "Benutzerdefiniert",
"label-verbose": "Ausführlich"
}
},
"promql": {
"getAggregationOptions": {
"documentation-avg": "Durchschnitt über Dimensionen berechnen",
"documentation-bottomk": "Kleinste k-Elemente nach Probenwert",
"documentation-count": "Anzahl der Elemente im Vektor zählen",
"documentation-count-values": "Anzahl der Elemente mit selbem Wert zählen",
"documentation-group": "Alle Werte im resultierenden Vektor sind 1",
"documentation-max": "Maximum über Dimensionen auswählen",
"documentation-min": "Minimum über Dimensionen auswählen",
"documentation-quantile": "φ-Quantil (0 ≤ φ ≤ 1) über Dimensionen berechnen",
"documentation-stddev": "Populationsstandardabweichung über Dimensionen berechnen",
"documentation-stdvar": "Populationsstandardvarianz über Dimensionen berechnen",
"documentation-sum": "Summe über Dimensionen berechnen",
"documentation-topk": "Größte k-Elemente nach Stichprobenwert"
},
"getFunctions": {
"documentation-abs": "Gibt den Eingabevektor mit allen Probenwerten zurück, die zu ihrem Absolutwert umgerechnet wurden.",
"documentation-absent": "Gibt einen leeren Vektor zurück, wenn der übergebene Vektor Elemente enthält, und einen 1-Element-Vektor mit dem Wert 1, wenn der übergebene Vektor keine Elemente enthält. Dies ist nützlich, um zu warnen, wenn für eine bestimmte Kombination aus Metrikname und Label keine Zeitreihen vorhanden sind.",
"documentation-absent-over-time": "Gibt einen leeren Vektor zurück, wenn der übergebene Bereichsvektor Elemente enthält, und einen 1-Element-Vektor mit dem Wert 1, wenn der übergebene Bereichsvektor keine Elemente enthält.",
"documentation-avg-over-time": "Der Durchschnittswert aller Punkte im angegebenen Intervall.",
"documentation-ceil": "Rundet die Probenwerte aller Elemente in „v“ auf die nächste ganze Zahl auf.",
"documentation-changes": "Für jede Eingabezeitreihe gibt „changes(v range-vector)“ die Anzahl der Änderungen seines Werts innerhalb des angegebenen Zeitbereichs als Momentanvektor zurück.",
"documentation-clamp": "Begrenzt die Probenwerte aller Elemente in „v“ auf eine Untergrenze von „min“ und eine Obergrenze von „max“.",
"documentation-clamp-max": "Begrenzt die Probenwerte aller Elemente in „v“ auf eine Obergrenze von „max“.",
"documentation-clamp-min": "Begrenzt die Probenwerte aller Elemente in „v“ auf eine Untergrenze von „min“.",
"documentation-count-over-time": "Die Anzahl aller Werte im angegebenen Intervall.",
"documentation-count-scalar": "Gibt die Anzahl der Elemente in einem Zeitreihenvektor als Skalar zurück. Dies steht im Gegensatz zum Aggregationsoperator „count()“, der immer einen Vektor zurückgibt (einen leeren, wenn der Eingabevektor leer ist) und die Gruppierung nach Labels über eine „by“-Klausel ermöglicht.",
"documentation-day-of-month": "Gibt den Tag des Monats für jede der angegebenen Zeiten in UTC zurück. Die zurückgegebenen Werte liegen zwischen 1 und 31.",
"documentation-day-of-week": "Gibt den Tag der Woche für jede der angegebenen Zeiten in UTC zurück. Die zurückgegebenen Werte liegen zwischen 0 und 6, wobei 0 für Sonntag steht usw.",
"documentation-day-of-year": "Gibt den Tag des Jahres für jede der angegebenen Zeiten in UTC zurück. Die zurückgegebenen Werte liegen zwischen 1 und 365 für Nicht-Schaltjahre und zwischen 1 und 366 für Schaltjahre.",
"documentation-days-in-month": "Gibt die Anzahl der Tage im Monat für jede der angegebenen Zeiten in UTC zurück. Die zurückgegebenen Werte liegen zwischen 28 und 31.",
"documentation-deg": "Rechnet Radianten in Grad um, bei allen Elementen in v",
"documentation-delta": "Berechnet die Differenz zwischen dem ersten und dem letzten Wert jedes Zeitreihenelements in einem Bereichsvektor „v“ und gibt einen Momentanvektor mit den angegebenen Deltas und äquivalenten Labels zurück. Das Delta wird extrapoliert, um den gesamten Zeitbereich zu umfassen, wie im Bereichsvektor-Selektor angegeben, sodass es möglich ist, ein nicht ganzzahliges Ergebnis zu erhalten, auch wenn die Probenwerte alle ganze Zahlen sind.",
"documentation-deriv": "Berechnet die Ableitung pro Sekunde der Zeitreihe in einem Bereichsvektor „v“ mithilfe einer einfachen linearen Regression.",
"documentation-double-exponential-smoothing": "Erzeugt einen geglätteten Wert für Zeitreihen basierend auf dem Bereich in „v“. Je niedriger der Glättungsfaktor „sf“ ist, desto mehr Bedeutung kommt alten Daten zu. Je höher der Trendfaktor „tf“ ist, desto mehr Trends in den Daten werden berücksichtigt. Sowohl „sf“ als auch „tf“ müssen zwischen 0 und 1 liegen.",
"documentation-drop-common-labels": "Löscht alle Labels, die den gleichen Namen und Wert über alle Reihen im Eingabevektor aufweisen.",
"documentation-exp": "Berechnet die Exponentialfunktion für alle Elemente in „v“.\nSonderfälle sind:\n* „Exp(+Inf) = +Inf“ \n* „Exp(NaN) = NaN“",
"documentation-floor": "Rundet die Probenwerte aller Elemente in „v“ auf die nächste ganze Zahl ab.",
"documentation-histogram-avg": "Gibt den arithmetischen Mittelwert der beobachteten Werte zurück, die in einem nativen Histogramm gespeichert sind. Proben, die keine nativen Histogramme sind, werden ignoriert und nicht im zurückgegebenen Vektor angezeigt.",
"documentation-histogram-count": "Gibt die Anzahl der in einem nativen Histogramm gespeicherten Beobachtungen zurück.",
"documentation-histogram-fraction": "Gibt den geschätzten Anteil der Beobachtungen zwischen den angegebenen unteren und oberen Werten zurück.",
"documentation-histogram-quantile": "Berechnet das φ-Quantil (0 ≤ φ ≤ 1) von den Buckets „b“ eines Histogramms. Die Proben in „b“ sind die Anzahl der Beobachtungen in jedem Bucket. Jede Probe muss ein Label „le“ haben, wobei der Labelwert die inklusive obere Grenze des Buckets angibt. (Proben ohne ein solches Label werden still ignoriert.) Der Histogramm-Metriktyp stellt automatisch Zeitreihen mit dem Suffix „_bucket“ und den entsprechenden Labels bereit.",
"documentation-histogram-stddev": "Gibt die geschätzte Standardabweichung der Beobachtungen in einem nativen Histogramm zurück, basierend auf dem geometrischen Mittel der Buckets, wo die Beobachtungen liegen.",
"documentation-histogram-stdvar": "Gibt die geschätzte Standardabweichung von Beobachtungen in einem nativen Histogramm zurück.",
"documentation-histogram-sum": "Gibt die Summe der in einem nativen Histogramm gespeicherten Beobachtungen zurück.",
"documentation-holt-winters": "Umbenannt in double_exponential_smoothing in Prometheus v3.x. Bitte verwenden Sie für Prometheus-Versionen gleich und höher als v3.0 double_exponential_smoothing. \n\nErzeugt einen geglätteten Wert für Zeitreihen, basierend auf dem Bereich in „v“. Je niedriger der Glättungsfaktor „sf“ ist, desto mehr Bedeutung kommt alten Daten zu. Je höher der Trendfaktor „tf“ ist, desto mehr Trends in den Daten werden berücksichtigt. Sowohl „sf“ als auch „tf“ müssen zwischen 0 und 1 liegen.",
"documentation-hour": "Gibt die Stunde des Tages für jede der angegebenen Zeiten in UTC zurück. Die zurückgegebenen Werte liegen zwischen 0 und 23.",
"documentation-idelta": "Berechnet die Differenz zwischen den letzten beiden Proben im Bereichsvektor „v“ und gibt einen Momentanvektor mit den angegebenen Deltas und äquivalenten Labels zurück.",
"documentation-increase": "Berechnet die Zunahme der Zeitreihe im Bereichsvektor. Brüche in der Monotonie (z. B. Zählerzurücksetzungen aufgrund von Zielneustarts) werden automatisch angepasst. Die Zunahme wird extrapoliert, um den gesamten Zeitbereich zu umfassen, wie im Bereichsvektor-Selektor angegeben, sodass es möglich ist, ein nicht ganzzahliges Ergebnis zu erhalten, auch wenn ein Zähler nur um ganzzahlige Inkremente zunimmt.",
"documentation-info": "Gibt die neuesten Details und Metadaten zu einer Gruppe von Metriken zurück, z. B. ihre Labels und aktuellen Werte, ohne Berechnungen durchzuführen",
"documentation-irate": "Berechnet die momentane Anstiegsrate (pro Sekunde) der Zeitreihe im Bereichsvektor. Dies basiert auf den letzten beiden Datenpunkten. Brüche in der Monotonie (z. B. Zählerzurücksetzungen aufgrund von Zielneustarts) werden automatisch angepasst.",
"documentation-label-join": "Verbindet für jede Zeitreihe in „v“ alle Werte aller „src_labels“ mit „separator“ und gibt die Zeitreihe mit dem Label „dst_label“ zurück, das den verbundenen Wert enthält. Es kann eine beliebige Anzahl von „src_labels“ in dieser Funktion geben.",
"documentation-label-replace": "Für jede Zeitreihe in „v“ gleicht „label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)“ den regulären Ausdruck „regex“ mit dem Label „src_label“ ab. Bei einer Übereinstimmung wird die Zeitreihe mit dem Label „dst_label“ zurückgegeben, das durch die Erweiterung von „replacement“ ersetzt wird. „$1“ wird durch die erste übereinstimmende Untergruppe ersetzt, „$2“ durch die zweite usw. Wenn der reguläre Ausdruck nicht übereinstimmt, wird die Zeitreihe unverändert zurückgegeben.",
"documentation-last-over-time": "Der letzte Punktwert im angegebenen Intervall.",
"documentation-ln": "Berechnet den natürlichen Logarithmus für alle Elemente in „v“.\nSonderfälle sind:\n * „ln(+Inf) = +Inf“\n * „ln(0) = -Inf“\n * „ln(x < 0) = NaN“\n * „ln(NaN) = NaN“",
"documentation-log10": "Berechnet den Dezimal-Logarithmus für alle Elemente in „v“. Die Sonderfälle entsprechen denen in „ln“.",
"documentation-log2": "Berechnet den binären Logarithmus für alle Elemente in „v“. Die Sonderfälle entsprechen denen in „ln“.",
"documentation-max-over-time": "Der Maximalwert aller Punkte im angegebenen Intervall.",
"documentation-min-over-time": "Der Minimalwert aller Punkte im angegebenen Intervall.",
"documentation-minute": "Gibt die Minute der Stunde für jede der angegebenen Zeiten in UTC zurück. Die zurückgegebenen Werte liegen zwischen 0 und 59.",
"documentation-month": "Gibt den Monat des Jahres für jede der angegebenen Zeiten in UTC zurück. Die zurückgegebenen Werte liegen zwischen 1 und 12, wobei 1 für Januar steht usw.",
"documentation-pi": "Gibt pi zurück",
"documentation-predict-linear": "Prognostiziert den Wert der Zeitreihe „t“ Sekunden ab jetzt, basierend auf dem Bereichsvektor „v“, mithilfe einer einfachen linearen Regression.",
"documentation-present-over-time": "Der Wert 1 für jede Reihe im angegebenen Intervall.",
"documentation-quantile-over-time": "Das φ-Quantil (0 ≤ φ ≤ 1) der Werte im angegebenen Intervall.",
"documentation-rad": "Rechnet Grad in Radianten um, bei allen Elementen in v",
"documentation-rate": "Berechnet die durchschnittliche Anstiegsrate (pro Sekunde) der Zeitreihe im Bereichsvektor. Brüche in der Monotonie (z. B. Zählerzurücksetzungen aufgrund von Zielneustarts) werden automatisch angepasst. Außerdem extrapoliert die Berechnung zu den Enden des Zeitbereichs, um ausgelassene Scrapes oder eine unvollkommene Ausrichtung der Scrape-Zyklen mit dem Zeitraum des Bereichs zu ermöglichen.",
"documentation-resets": "Für jede Eingabezeitreihe gibt „resets(v range-vector)“ die Anzahl der Zählerzurücksetzungen innerhalb des angegebenen Zeitbereichs als Momentanvektor zurück. Jede Abnahme des Werts zwischen zwei aufeinanderfolgenden Proben wird als Zählerzurücksetzung interpretiert.",
"documentation-round": "Rundet die Probenwerte aller Elemente in „v“ auf die nächste ganze Zahl. Gleichstände werden durch Aufrunden gelöst. Mit dem optionalen Argument „to_nearest“ können Sie das nächste Vielfache angeben, auf das die Probenwerte gerundet werden sollen. Dieses Vielfache kann auch ein Bruch sein.",
"documentation-scalar": "Bei einem Eingabevektor mit einem einzelnen Element gibt „scalar(v instant-vector)“ den Probenwert dieses einzelnen Elements als Skalar zurück. Wenn der Eingabevektor nicht genau ein Element hat, gibt „scalar“ „NaN“ zurück.",
"documentation-sgn": "Gibt einen Vektor mit allen Probenwerten zurück, die in ihr Vorzeichen umgewandelt wurden, wie folgt definiert: 1, wenn v positiv ist, -1, wenn v negativ ist und 0, wenn v gleich Null ist.",
"documentation-sort": "Gibt Vektorelemente zurück, die in aufsteigender Reihenfolge nach ihren Probenwerten sortiert sind.",
"documentation-sort-desc": "Gibt Vektorelemente zurück, die in absteigender Reihenfolge nach ihren Probenwerten sortiert sind.",
"documentation-sqrt": "Berechnet die Quadratwurzel aller Elemente in „v“.",
"documentation-stddev-over-time": "Die Populationsstandardabweichung der Werte im angegebenen Intervall.",
"documentation-stdvar-over-time": "Die Populationsstandardvarianz der Werte im angegebenen Intervall.",
"documentation-sum-over-time": "Die Summe aller Werte im angegebenen Intervall.",
"documentation-time": "Gibt die Anzahl der Sekunden seit dem 1. Januar 1970 UTC zurück. Beachten Sie, dass dies nicht tatsächlich die aktuelle Zeit wiedergibt, sondern den Zeitpunkt, zu dem der Ausdruck ausgewertet werden soll.",
"documentation-timestamp": "Gibt den Zeitstempel jeder der Proben des angegebenen Vektors als Anzahl der Sekunden seit dem 1. Januar 1970 UTC zurück.",
"documentation-vector": "Gibt den Skalar „s“ als Vektor ohne Labels zurück.",
"documentation-year": "Gibt das Jahr für jede der angegebenen Zeiten in UTC zurück."
},
"getTrigonometricFunctions": {
"documentation-acos": "berechnet den Arkuskosinus aller Elemente in {{argument}}",
"documentation-acosh": "berechnet den inversen hyperbolischen Kosinus aller Elemente in {{argument}}",
"documentation-asin": "berechnet den Arkussinus aller Elemente in {{argument}}",
"documentation-asinh": "berechnet den inversen hyperbolischen Sinus aller Elemente in {{argument}}",
"documentation-atan": "berechnet den Arkustangens aller Elemente in {{argument}}",
"documentation-atanh": "berechnet den inversen hyperbolischen Tangens aller Elemente in {{argument}}",
"documentation-cos": "berechnet den Kosinus aller Elemente in {{argument}}",
"documentation-cosh": "berechnet den hyperbolischen Kosinus aller Elemente in {{argument}}",
"documentation-sin": "berechnet den Sinus aller Elemente in {{argument}}",
"documentation-sinh": "berechnet den hyperbolischen Sinus aller Elemente in {{argument}}",
"documentation-tan": "berechnet den Tangens aller Elemente in {{argument}}",
"documentation-tanh": "berechnet den hyperbolischen Tangens aller Elemente in {{argument}}"
}
},
"querybuilder": {
"feedback-link": {
"give-feedback": "Feedback geben",
"title-give-feedback": "Der Metrik-Explorer ist neu. Bitte teilen Sie uns mit, wie wir ihn verbessern können."
},
"get-collapsed-info": {
"exemplars": "Exemplare: {{value}}",
"format": "Format: {{value}}",
"legend": "Legende: {{value}}",
"step": "Schritt: {{value}}",
"type": "Typ: {{value}}"
},
"get-placeholders": {
"browse": "Metriken nach Namen suchen",
"type": "Nach Typ filtern"
},
"get-prom-types": {
"description-counter": "Eine kumulative Metrik, die einen einzelnen monoton ansteigenden Zähler darstellt, dessen Wert nur ansteigen oder beim Neustart auf Null zurückgesetzt werden kann.",
"description-gauge": "Eine Metrik, die einen einzelnen numerischen Wert darstellt, der beliebig nach oben und unten verlaufen kann.",
"description-histogram": "Ein Histogramm fragt Beobachtungen ab (in der Regel Dinge wie Anfragedauer oder Antwortgrößen) und zählt sie in konfigurierbaren Buckets.",
"description-native-histogram": "Native Histogramme unterscheiden sich in mehrerer Hinsicht von klassischen Prometheus-Histogrammen: Die Grenzen der nativen Histogramm-Buckets werden durch eine Formel berechnet, die von der Skala (Auflösung) des nativen Histogramms abhängt, und sind nicht benutzerdefiniert.",
"description-no-type": "Diese Metriken haben keinen festgelegten Typ in den Metadaten.",
"description-summary": "Eine Zusammenfassung fragt Beobachtungen ab (in der Regel Dinge wie Anfragedauer und Antwortgrößen) und kann konfigurierbare Quantile über ein gleitendes Zeitfenster berechnen.",
"description-unknown": "Diesen Metriken wurde in den Metadaten der Typ „unbekannt“ zugewiesen.",
"label-counter": "Zähler",
"label-gauge": "Anzeige",
"label-histogram": "Histogramm",
"label-native-histogram": "Natives Histogramm",
"label-no-type": "Kein Typ",
"label-summary": "Zusammenfassung",
"label-unknown": "Unbekannt"
},
"handle-function": {
"text": {
"query-parsing-is-ambiguous": "Das Abfrage-Parsing ist unklar."
}
},
"label-filter-item": {
"aria-label-remove": "Entfernen von {{name}}",
"placeholder-select-label": "Label auswählen",
"placeholder-select-value": "Wert auswählen"
},
"label-filters": {
"label-filters": "Label-Filter",
"label-label-filters": "Label-Filter",
"tooltip-label-filters": "Optional: wird verwendet, um die Metrikauswahl für diesen Abfragetyp zu filtern."
},
"label-param-editor": {
"loadingMessage-loading-labels": "Labels werden geladen",
"noOptionsMessage-no-labels-found": "Keine Labels gefunden"
},
"metric-combobox": {
"async-select": {
"aria-label-open-metrics-explorer": "Metrik-Explorer öffnen",
"placeholder-select-metric": "Metrik auswählen",
"tooltip-open-metrics-explorer": "Metrik-Explorer öffnen"
},
"label-metric": "Metrik",
"tooltip-metric": "Optional: gibt eine Liste von Label-Werten für den Label-Namen in der angegebenen Metrik zurück."
},
"metrics-modal": {
"aria-label-browse-metrics": "Metriken durchsuchen",
"currently-selected": "Aktuell ausgewählt: {{selected}}",
"metrics-pre-filtered": "Diese Metriken wurden anhand der Labels, die in den Label-Filtern ausgewählt wurden, vorgefiltert.",
"title-metrics-explorer": "Metrik-Explorer"
},
"nested-query": {
"label": {
"ignoring": "Ignorieren",
"on": "Aktiviert"
},
"operator": "Bediener",
"tooltip-remove-match": "Übereinstimmung entfernen",
"vector-matches": "Vektorübereinstimmungen"
},
"operation-editor": {
"not-found": "Vorgang {{id}} nicht gefunden",
"title-remove": "Entfernen von {{name}}"
},
"operation-header": {
"placeholder-replace-with": "Ersetzen mit",
"title-click-to-view-alternative-operations": "Klicken Sie zur Anzeige alternativer Vorgänge",
"title-remove-operation": "Vorgang entfernen"
},
"operation-info-button": {
"title-click-to-show-description": "Klicken Sie zur Ansicht der Beschreibung",
"title-remove-operation": "Operation entfernen"
},
"operation-list": {
"operations": "Vorgänge",
"placeholder-search": "Suche",
"title-add-operation": "Vorgang hinzufügen"
},
"operation-param-editor": {
"title-add": "{{name}} hinzufügen",
"title-remove": "{{name}} entfernen"
},
"operation-utils": {
"getAggregationExplainer": {
"label-by": "Berechnet {{aggregationName}} über Dimensionen unter Beibehaltung von {{labelWord}} {{labels}}.",
"label-default": "Berechnet {{aggregationName}} über die Dimensionen.",
"label-without": "Berechnet {{aggregationName}} über die Dimensionen {{labels}}. Alle anderen Labels werden beibehalten."
}
},
"prom-query-builder-options": {
"aria-label-exemplars": "Exemplarwechsel.",
"aria-label-format": "Format Kombinationsfeld",
"aria-label-lower-limit-parameter": "Textfeld „Min. Schritt“, Untergrenze für den Schrittparameter festlegen",
"aria-label-select-resolution": "Auflösung auswählen",
"aria-label-type": "Typ-Radiobutton-Gruppe",
"format-options": {
"label-heatmap": "Heatmap",
"label-table": "Tabelle",
"label-time-series": "Zeitreihen"
},
"label-exemplars": "Exemplare",
"label-format": "Format",
"label-min-step": "Min. Schritt",
"label-resolution": "Auflösung",
"label-type": "Typ",
"placeholder-auto": "Auto",
"title-options": "Optionen",
"tooltip-min-step": "Eine zusätzliche Untergrenze für den Schrittparameter der Prometheus-Abfrage und für die Variablen <2>{{interval}}</2> und <4>{{rateInterval}}</4>."
},
"prom-query-editor-selector": {
"body-syntax-error": "Es liegt ein Syntaxfehler vor oder die Abfragestruktur kann beim Wechsel in den Builder-Modus nicht visualisiert werden. Teile der Abfrage können verlorengehen.",
"confirmText-continue": "Fortfahren",
"kick-start-your-query": "Starten Sie Ihre Abfrage",
"label-explain": "Erklären",
"run-queries": "Abfragen ausführen",
"title-parsing-error-switch-builder": "Parsing-Fehler: In den Builder-Modus wechseln?"
},
"prom-query-legend-editor": {
"aria-label-legend": "Legende Kombinationsfeld",
"label-legend": "Legende",
"placeholder-select-legend-mode": "Legendenmodus auswählen",
"tooltip-legend": "Überschreibung des Reihennamens oder Vorlage. Beispiel: {{templateExample}} wird durch den Label-Wert für {{labelName}} ersetzt."
},
"query-builder-hints": {
"hint-details": "Hinweis: {{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "Builder",
"label-code": "Code"
}
},
"query-pattern": {
"apply-query": "Abfrage anwenden",
"aria-label-apply-query-starter-button": "Schaltfläche zur Anwendung des Abfrage-Starters",
"aria-label-back-button": "Zurück-Schaltfläche",
"aria-label-create-new-query-button": "Schaltfläche zur Erstellung einer neuen Abfrage",
"aria-label-raw-query": "{{patternName}} Rohabfrage",
"aria-label-use-this-query-button": "Schaltfläche zum Nutzen dieser Abfrage",
"back": "Zurück",
"create-new-query": "Neue Abfrage erstellen",
"use-this-query": "Diese Abfrage nutzen"
},
"query-patterns-modal": {
"aria-label-close-kick-start-your-query-modal": "Modal zum Starten Ihrer Abfrage schließen",
"aria-label-kick-start-your-query-modal": "Starten Sie Ihr Abfrage-Modal",
"aria-label-toggle-query-starter": "Öffnen und Schließen der {{patternType}} Abfrage-Starter-Karte",
"close": "Schließen",
"description-kick-start-your-query": "Starten Sie Ihre Abfrage, indem Sie eine dieser Abfragen auswählen. Sie können dann mit dem Abschluss Ihrer Abfrage fortfahren.",
"label-toggle-query-starter": "{{patternType}} Abfrage-Starter",
"title-kick-start-your-query": "Starten Sie Ihre Abfrage"
},
"raw-query": {
"aria-label-selector": "Selektor"
},
"results-table": {
"content-descriptive-type": "Bei der Erstellung von {{descriptiveType}} zeigt Prometheus mehrere Reihen mit dem Typ Counter. ",
"description": "Beschreibung",
"message-expand-label-filters": "Es wurden keine Metriken gefunden. Versuchen Sie, Ihre Label-Filter zu erweitern.",
"message-expand-search": "Es wurden keine Metriken gefunden. Versuchen Sie, Ihre Suche und Ihre Filter zu erweitern.",
"message-no-metrics-found": "Es wurden keine Metriken in der Datenquelle gefunden.",
"name": "Name",
"type": "Typ"
}
}
}
}

View File

@@ -1,492 +0,0 @@
{
"grafana-prometheus": {
"components": {
"annotation-query-editor": {
"annotation-data-load-error": "Annotation data load error!",
"aria-label-lower-limit-parameter": "Set lower limit for the step parameter",
"label-min-step": "Min step",
"label-series-value-as-timestamp": "Series value as timestamp",
"label-tags": "Tags",
"label-text": "Text",
"label-title": "Title",
"placeholder-auto": "auto",
"tooltip-either-pattern-example-instance-replaced-label": "Use either the name or a pattern. For example, {{labelTemplate}} is replaced with label value for the label {{labelName}}.",
"tooltip-min-step": "An additional lower limit for the step parameter of the Prometheus query and for the <2>{{intervalVar}}</2> and <4>{{rateIntervalVar}}</4> variables.",
"tooltip-timestamp-milliseconds-series-value-seconds-multiply": "The unit of timestamp is milliseconds. If the unit of the series value is seconds, multiply its range vector by 1000."
},
"get-query-type-options": {
"description": {
"instant-query-range": "Run an Instant query and a Range query"
},
"label": {
"both": "Both"
},
"range-options": {
"description": {
"query-range": "Run query over a range of time"
},
"label": {
"instant": "Instant",
"range": "Range"
}
}
},
"label-selector": {
"aria-label-filter-expression-for-label": "Filter expression for label",
"description-select-labels": "Once label values are selected, only possible label combinations are shown.",
"select-labels-to-search-in": "2. Select labels to search in"
},
"metric-selector": {
"aria-label-filter-expression-for-metric": "Filter expression for metric",
"aria-label-limit-results-from-series-endpoint": "Limit results from series endpoint",
"description-series-limit": "The limit applies to all metrics, labels, and values. Leave the field empty to use the default limit. Set to 0 to disable the limit and fetch everything — this may cause performance issues.",
"label-select-metric": "Once a metric is selected only possible labels are shown. Labels are limited by the series limit below.",
"select-a-metric": "1. Select a metric",
"series-limit": "Series limit"
},
"prom-cheat-sheet": {
"prom-ql-cheat-sheet": "PromQL Cheat Sheet"
},
"prom-exemplar-field": {
"exemplars": "Exemplars",
"tooltip-disable-query": "Disable query with exemplars",
"tooltip-enable-query": "Enable query with exemplars"
},
"prom-explore-extra-field": {
"aria-label-prometheus-extra-field": "Prometheus extra field",
"aria-label-query-type-field": "Query type field",
"aria-label-step-field": "Step field",
"min-step": "Min step",
"query-type": "Query type",
"tooltip-units-builtin-variables-example-interval-rateinterval": "Time units and built-in variables can be used here, for example: {{example1}}, {{example2}}, {{example3}}, {{example4}}, {{example5}}, {{example6}}, {{example7}} (Default if no unit is specified: {{default}})"
},
"prom-query-field": {
"placeholder-enter-a-prom-ql-query": "Enter a PromQL query…"
},
"prom-variable-query-editor": {
"aria-label-classic-query": "Classic Query",
"aria-label-metric-regex": "Metric regex",
"aria-label-metric-selector": "Metric selector",
"aria-label-prometheus-query": "Prometheus Query",
"aria-label-query-type": "Query type",
"aria-label-series-query": "Series Query",
"label-classic-query": "Classic Query",
"label-label": "Label",
"label-metric-regex": "Metric regex",
"label-query": "Query",
"label-query-type": "Query type",
"label-series-query": "Series Query",
"placeholder-classic-query": "Classic Query",
"placeholder-metric-regex": "Metric regex",
"placeholder-prometheus-query": "Prometheus Query",
"placeholder-select-query-type": "Select query type",
"placeholder-series-query": "Series Query",
"returns-metrics-matching-specified-metric-regex": "Returns a list of metrics matching the specified metric regex.",
"tooltip-classic-query": "The original implementation of the Prometheus variable query editor. Enter a string with the correct query type and parameters as described in these docs. For example, {{exampleQuery}}.",
"tooltip-label": "Returns a list of label values for the label name in all metrics unless the metric is specified.",
"tooltip-metric-regex": "Returns a list of label names, optionally filtering by specified metric regex.",
"tooltip-query": "Returns a list of Prometheus query results for the query. This can include Prometheus functions, i.e.{{exampleQuery}}.",
"tooltip-query-type": "The Prometheus data source plugin provides the following query types for template variables.",
"tooltip-series-query": "Enter a metric with labels, only a metric or only labels, i.e.{{example1}}, {{example2}}, or {{example3}}. Returns a list of time series associated with the entered data."
},
"selector-actions": {
"aria-label-selector": "selector",
"aria-label-selector-clear-button": "Selector clear button",
"aria-label-use-selector-as-metrics-button": "Use selector as metrics button",
"aria-label-use-selector-for-query-button": "Use selector for query button",
"aria-label-validate-submit-button": "Validate submit button",
"clear": "Clear",
"resulting-selector": "4. Resulting selector",
"use-as-rate-query": "Use as rate query",
"use-query": "Use query",
"validate-selector": "Validate selector"
},
"value-selector": {
"aria-label-filter-expression-for-label-values": "Filter expression for label values",
"aria-label-values-for": "Values for {{labelKey}}",
"description-search-field-values-across-selected-labels": "Use the search field to find values across selected labels.",
"select-multiple-values-for-your-labels": "3. Select (multiple) values for your labels"
}
},
"configuration": {
"alerting-settings-overhaul": {
"label-allow-as-recording-rules-target": "Allow as recording rules target",
"label-manage-alerts-via-alerting-ui": "Manage alerts via Alerting UI",
"title-alerting": "Alerting",
"tooltip-allow-as-recording-rules-target": "Allow this data source to be selected as a target for writing recording rules.",
"tooltip-manage-alerts-via-alerting-ui": "Manage alert rules for this data source. To manage other alerting resources, add an Alertmanager data source."
},
"config-editor": {
"browser-access-mode-error": "Browser access mode in the Prometheus data source is no longer available. Switch to server access mode.",
"description-advanced-settings": "Additional settings are optional settings that can be configured for more control over your data source.",
"title-advanced-settings": "Advanced settings",
"title-error": "Error"
},
"data-source-http-settings-overhaul": {
"tooltip-browser-access-mode": "Your access method is <1>Browser</1>, this means the URL needs to be accessible from the browser.",
"tooltip-http-url": "Specify a complete HTTP URL (for example {{exampleURL}})",
"tooltip-server-access-mode": "Your access method is <1>Server</1>, this means the URL needs to be accessible from the grafana backend/server."
},
"docs-tip": {
"visit-docs-for-more-details-here": "Visit docs for more details here."
},
"exemplar-setting": {
"label-data-source": "Data source",
"label-internal-link": "Internal link",
"label-label-name": "Label name",
"label-remove-exemplar-link": "Remove exemplar link",
"label-url": "URL",
"label-url-label": "URL Label",
"placeholder-go-to-examplecom": "Go to example.com",
"placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}",
"placeholder-trace-id": "traceID",
"title-remove-exemplar-link": "Remove exemplar link",
"tooltip-data-source": "The data source the exemplar is going to navigate to.",
"tooltip-internal-link": "Enable this option if you have an internal link. When enabled, this reveals the data source selector. Select the backend tracing data store for your exemplar data.",
"tooltip-label-name": "The name of the field in the labels object that should be used to get the traceID.",
"tooltip-url": "The URL of the trace backend the user would go to see its trace",
"tooltip-url-label": "Use to override the button label on the exemplar traceID field."
},
"exemplars-settings": {
"add": "Add",
"no-exemplars-configurations": "No exemplars configurations",
"title-exemplars": "Exemplars"
},
"prom-settings": {
"aria-label-default-editor": "Default Editor (Code or Builder)",
"aria-label-prom-type-type": "{{promType}} type",
"aria-label-prometheus-type": "Prometheus type",
"aria-label-select-http-method": "Select HTTP method",
"editor-options": {
"label-builder": "Builder",
"label-code": "Code"
},
"label-cache-level": "Cache level",
"label-custom-query-parameters": "Custom query parameters",
"label-default-editor": "Default editor",
"label-disable-metrics-lookup": "Disable metrics lookup",
"label-disable-recording-rules-beta": "Disable recording rules (beta)",
"label-http-method": "HTTP method",
"label-incremental-querying-beta": "Incremental querying (beta)",
"label-prom-type-version": "{{promType}} version",
"label-prometheus-type": "Prometheus type",
"label-query-overlap-window": "Query overlap window",
"label-query-timeout": "Query timeout",
"label-scrape-interval": "Scrape interval",
"label-series-limit": "Series limit",
"label-use-series-endpoint": "Use series endpoint",
"more-info": "For more information on configuring prometheus type and version in data sources, see the <2>provisioning documentation</2>.",
"placeholder-example-maxsourceresolutionmtimeout": "Example: {{example}}",
"title-interval-behaviour": "Interval behaviour",
"title-other": "Other",
"title-performance": "Performance",
"title-query-editor": "Query editor",
"tooltip-cache-level": "Sets the browser caching level for editor queries. Higher cache settings are recommended for high cardinality data sources.",
"tooltip-custom-query-parameters": "Add custom parameters to the Prometheus query URL. For example {{example1}}, {{example2}}, {{example3}}, or{{example4}}. Multiple parameters should be concatenated together with {{concatenationChar}}.",
"tooltip-default-editor": "Set default editor option for all users of this data source.",
"tooltip-disable-metrics-lookup": "Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. ",
"tooltip-disable-recording-rules-beta": "This feature will disable recording rules. Turn this on to improve dashboard performance",
"tooltip-http-method": "You can use either POST or GET HTTP method to query your Prometheus data source. POST is the recommended method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network.",
"tooltip-incremental-querying-beta": "This feature will change the default behavior of relative queries to always request fresh data from the prometheus instance, instead query results will be cached, and only new records are requested. Turn this on to decrease database and network load.",
"tooltip-prom-type-version": "Use this to set the version of your {{promType}} instance if it is not automatically configured.",
"tooltip-prometheus-type": "Set this to the type of your prometheus database, e.g. Prometheus, Cortex, Mimir or Thanos. Changing this field will save your current settings. Certain types of Prometheus supports or does not support various APIs. For example, some types support regex matching for label queries to improve performance. Some types have an API for metadata. If you set this incorrectly you may experience odd behavior when querying metrics and labels. Please check your Prometheus documentation to ensure you enter the correct type.",
"tooltip-query-overlap-window": "Set a duration like {{example1}} or {{example2}} or {{example3}}. Default of {{default}}. This duration will be added to the duration of each incremental request.",
"tooltip-query-timeout": "Set the Prometheus query timeout.",
"tooltip-scrape-interval": "This interval is how frequently Prometheus scrapes targets. Set this to the typical scrape and evaluation interval configured in your Prometheus config file. If you set this to a greater value than your Prometheus config file interval, Grafana will evaluate the data according to this interval and you will see less data points. Defaults to {{default}}.",
"tooltip-series-limit": "The limit applies to all resources (metrics, labels, and values) for both endpoints (series and labels). Leave the field empty to use the default limit (40000). Set to 0 to disable the limit and fetch everything — this may cause performance issues. Default limit is 40000.",
"tooltip-use-series-endpoint": "Checking this option will favor the series endpoint with {{exampleParameter}} parameter over the label values endpoint with {{exampleParameter}} parameter. While the label values endpoint is considered more performant, some users may prefer the series because it has a POST method while the label values endpoint only has a GET method."
}
},
"metrics-browser": {
"disabled-label": "(Disabled)",
"enabled-label": "Metrics browser"
},
"prom-query-legend-editor": {
"get-legend-mode-options": {
"description-auto": "Only includes unique labels",
"description-custom": "Provide a naming template",
"description-verbose": "All label names and values",
"label-auto": "Auto",
"label-custom": "Custom",
"label-verbose": "Verbose"
}
},
"promql": {
"getAggregationOptions": {
"documentation-avg": "Calculate the average over dimensions",
"documentation-bottomk": "Smallest k elements by sample value",
"documentation-count": "Count number of elements in the vector",
"documentation-count-values": "Count number of elements with the same value",
"documentation-group": "All values in the resulting vector are 1",
"documentation-max": "Select maximum over dimensions",
"documentation-min": "Select minimum over dimensions",
"documentation-quantile": "Calculate φ-quantile (0 ≤ φ ≤ 1) over dimensions",
"documentation-stddev": "Calculate population standard deviation over dimensions",
"documentation-stdvar": "Calculate population standard variance over dimensions",
"documentation-sum": "Calculate sum over dimensions",
"documentation-topk": "Largest k elements by sample value"
},
"getFunctions": {
"documentation-abs": "Returns the input vector with all sample values converted to their absolute value.",
"documentation-absent": "Returns an empty vector if the vector passed to it has any elements and a 1-element vector with the value 1 if the vector passed to it has no elements. This is useful for alerting on when no time series exist for a given metric name and label combination.",
"documentation-absent-over-time": "Returns an empty vector if the range vector passed to it has any elements and a 1-element vector with the value 1 if the range vector passed to it has no elements.",
"documentation-avg-over-time": "The average value of all points in the specified interval.",
"documentation-ceil": "Rounds the sample values of all elements in `v` up to the nearest integer.",
"documentation-changes": "For each input time series, `changes(v range-vector)` returns the number of times its value has changed within the provided time range as an instant vector.",
"documentation-clamp": "Clamps the sample values of all elements in `v` to have a lower limit of `min` and an upper limit of `max`.",
"documentation-clamp-max": "Clamps the sample values of all elements in `v` to have an upper limit of `max`.",
"documentation-clamp-min": "Clamps the sample values of all elements in `v` to have a lower limit of `min`.",
"documentation-count-over-time": "The count of all values in the specified interval.",
"documentation-count-scalar": "Returns the number of elements in a time series vector as a scalar. This is in contrast to the `count()` aggregation operator, which always returns a vector (an empty one if the input vector is empty) and allows grouping by labels via a `by` clause.",
"documentation-day-of-month": "Returns the day of the month for each of the given times in UTC. Returned values are from 1 to 31.",
"documentation-day-of-week": "Returns the day of the week for each of the given times in UTC. Returned values are from 0 to 6, where 0 means Sunday etc.",
"documentation-day-of-year": "Returns the day of the year for each of the given times in UTC. Returned values are from 1 to 365 for non-leap years, and 1 to 366 in leap years.",
"documentation-days-in-month": "Returns number of days in the month for each of the given times in UTC. Returned values are from 28 to 31.",
"documentation-deg": "Converts radians to degrees for all elements in v",
"documentation-delta": "Calculates the difference between the first and last value of each time series element in a range vector `v`, returning an instant vector with the given deltas and equivalent labels. The delta is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if the sample values are all integers.",
"documentation-deriv": "Calculates the per-second derivative of the time series in a range vector `v`, using simple linear regression.",
"documentation-double-exponential-smoothing": "Produces a smoothed value for time series based on the range in `v`. The lower the smoothing factor `sf`, the more importance is given to old data. The higher the trend factor `tf`, the more trends in the data is considered. Both `sf` and `tf` must be between 0 and 1.",
"documentation-drop-common-labels": "Drops all labels that have the same name and value across all series in the input vector.",
"documentation-exp": "Calculates the exponential function for all elements in `v`.\nSpecial cases are:\n* `Exp(+Inf) = +Inf` \n* `Exp(NaN) = NaN`",
"documentation-floor": "Rounds the sample values of all elements in `v` down to the nearest integer.",
"documentation-histogram-avg": "Returns the arithmetic average of observed values stored in a native histogram. Samples that are not native histograms are ignored and do not show up in the returned vector.",
"documentation-histogram-count": "Returns the count of observations stored in a native histogram.",
"documentation-histogram-fraction": "Returns the estimated fraction of observations between the provided lower and upper values.",
"documentation-histogram-quantile": "Calculates the φ-quantile (0 ≤ φ ≤ 1) from the buckets `b` of a histogram. The samples in `b` are the counts of observations in each bucket. Each sample must have a label `le` where the label value denotes the inclusive upper bound of the bucket. (Samples without such a label are silently ignored.) The histogram metric type automatically provides time series with the `_bucket` suffix and the appropriate labels.",
"documentation-histogram-stddev": "Returns the estimated standard deviation of observations in a native histogram, based on the geometric mean of the buckets where the observations lie.",
"documentation-histogram-stdvar": "Returns the estimated standard variance of observations in a native histogram.",
"documentation-histogram-sum": "Returns the sum of observations stored in a native histogram.",
"documentation-holt-winters": "Renamed as double_exponential_smoothing in prometheus v3.x. For prometheus versions equal and greater than v3.0 please use double_exponential_smoothing. \n\nProduces a smoothed value for time series based on the range in `v`. The lower the smoothing factor `sf`, the more importance is given to old data. The higher the trend factor `tf`, the more trends in the data is considered. Both `sf` and `tf` must be between 0 and 1.",
"documentation-hour": "Returns the hour of the day for each of the given times in UTC. Returned values are from 0 to 23.",
"documentation-idelta": "Calculates the difference between the last two samples in the range vector `v`, returning an instant vector with the given deltas and equivalent labels.",
"documentation-increase": "Calculates the increase in the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. The increase is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if a counter increases only by integer increments.",
"documentation-info": "Returns latest details and metadata about a group of metrics, such as their labels and current values, without doing any calculations",
"documentation-irate": "Calculates the per-second instant rate of increase of the time series in the range vector. This is based on the last two data points. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for.",
"documentation-label-join": "For each timeseries in `v`, joins all the values of all the `src_labels` using `separator` and returns the timeseries with the label `dst_label` containing the joined value. There can be any number of `src_labels` in this function.",
"documentation-label-replace": "For each timeseries in `v`, `label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)` matches the regular expression `regex` against the label `src_label`. If it matches, then the timeseries is returned with the label `dst_label` replaced by the expansion of `replacement`. `$1` is replaced with the first matching subgroup, `$2` with the second etc. If the regular expression doesn't match then the timeseries is returned unchanged.",
"documentation-last-over-time": "The most recent point value in specified interval.",
"documentation-ln": "Calculates the natural logarithm for all elements in `v`.\nSpecial cases are:\n * `ln(+Inf) = +Inf`\n * `ln(0) = -Inf`\n * `ln(x < 0) = NaN`\n * `ln(NaN) = NaN`",
"documentation-log10": "Calculates the decimal logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.",
"documentation-log2": "Calculates the binary logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.",
"documentation-max-over-time": "The maximum value of all points in the specified interval.",
"documentation-min-over-time": "The minimum value of all points in the specified interval.",
"documentation-minute": "Returns the minute of the hour for each of the given times in UTC. Returned values are from 0 to 59.",
"documentation-month": "Returns the month of the year for each of the given times in UTC. Returned values are from 1 to 12, where 1 means January etc.",
"documentation-pi": "Returns pi",
"documentation-predict-linear": "Predicts the value of time series `t` seconds from now, based on the range vector `v`, using simple linear regression.",
"documentation-present-over-time": "The value 1 for any series in the specified interval.",
"documentation-quantile-over-time": "The φ-quantile (0 ≤ φ ≤ 1) of the values in the specified interval.",
"documentation-rad": "Converts degrees to radians for all elements in v",
"documentation-rate": "Calculates the per-second average rate of increase of the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of scrape cycles with the range's time period.",
"documentation-resets": "For each input time series, `resets(v range-vector)` returns the number of counter resets within the provided time range as an instant vector. Any decrease in the value between two consecutive samples is interpreted as a counter reset.",
"documentation-round": "Rounds the sample values of all elements in `v` to the nearest integer. Ties are resolved by rounding up. The optional `to_nearest` argument allows specifying the nearest multiple to which the sample values should be rounded. This multiple may also be a fraction.",
"documentation-scalar": "Given a single-element input vector, `scalar(v instant-vector)` returns the sample value of that single element as a scalar. If the input vector does not have exactly one element, `scalar` will return `NaN`.",
"documentation-sgn": "Returns a vector with all sample values converted to their sign, defined as this: 1 if v is positive, -1 if v is negative and 0 if v is equal to zero.",
"documentation-sort": "Returns vector elements sorted by their sample values, in ascending order.",
"documentation-sort-desc": "Returns vector elements sorted by their sample values, in descending order.",
"documentation-sqrt": "Calculates the square root of all elements in `v`.",
"documentation-stddev-over-time": "The population standard deviation of the values in the specified interval.",
"documentation-stdvar-over-time": "The population standard variance of the values in the specified interval.",
"documentation-sum-over-time": "The sum of all values in the specified interval.",
"documentation-time": "Returns the number of seconds since January 1, 1970 UTC. Note that this does not actually return the current time, but the time at which the expression is to be evaluated.",
"documentation-timestamp": "Returns the timestamp of each of the samples of the given vector as the number of seconds since January 1, 1970 UTC.",
"documentation-vector": "Returns the scalar `s` as a vector with no labels.",
"documentation-year": "Returns the year for each of the given times in UTC."
},
"getTrigonometricFunctions": {
"documentation-acos": "calculates the arccosine of all elements in {{argument}}",
"documentation-acosh": "calculates the inverse hyperbolic cosine of all elements in {{argument}}",
"documentation-asin": "calculates the arcsine of all elements in {{argument}}",
"documentation-asinh": "calculates the inverse hyperbolic sine of all elements in {{argument}}",
"documentation-atan": "calculates the arctangent of all elements in {{argument}}",
"documentation-atanh": "calculates the inverse hyperbolic tangent of all elements in {{argument}}",
"documentation-cos": "calculates the cosine of all elements in {{argument}}",
"documentation-cosh": "calculates the hyperbolic cosine of all elements in {{argument}}",
"documentation-sin": "calculates the sine of all elements in {{argument}}",
"documentation-sinh": "calculates the hyperbolic sine of all elements in {{argument}}",
"documentation-tan": "calculates the tangent of all elements in {{argument}}",
"documentation-tanh": "calculates the hyperbolic tangent of all elements in {{argument}}"
}
},
"querybuilder": {
"feedback-link": {
"give-feedback": "Give feedback",
"title-give-feedback": "The metrics explorer is new, please let us know how we can improve it"
},
"get-collapsed-info": {
"exemplars": "Exemplars: {{value}}",
"format": "Format: {{value}}",
"legend": "Legend: {{value}}",
"step": "Step: {{value}}",
"type": "Type: {{value}}"
},
"get-placeholders": {
"browse": "Search metrics by name",
"type": "Filter by type"
},
"get-prom-types": {
"description-counter": "A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.",
"description-gauge": "A metric that represents a single numerical value that can arbitrarily go up and down.",
"description-histogram": "A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.",
"description-native-histogram": "Native histograms are different from classic Prometheus histograms in a number of ways: Native histogram bucket boundaries are calculated by a formula that depends on the scale (resolution) of the native histogram, and are not user defined.",
"description-no-type": "These metrics have no defined type in the metadata.",
"description-summary": "A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.",
"description-unknown": "These metrics have been given the type unknown in the metadata.",
"label-counter": "Counter",
"label-gauge": "Gauge",
"label-histogram": "Histogram",
"label-native-histogram": "Native histogram",
"label-no-type": "No type",
"label-summary": "Summary",
"label-unknown": "Unknown"
},
"handle-function": {
"text": {
"query-parsing-is-ambiguous": "Query parsing is ambiguous."
}
},
"label-filter-item": {
"aria-label-remove": "Remove {{name}}",
"placeholder-select-label": "Select label",
"placeholder-select-value": "Select value"
},
"label-filters": {
"label-filters": "Label filters",
"label-label-filters": "Label filters",
"tooltip-label-filters": "Optional: used to filter the metric select for this query type."
},
"label-param-editor": {
"loadingMessage-loading-labels": "Loading labels",
"noOptionsMessage-no-labels-found": "No labels found"
},
"metric-combobox": {
"async-select": {
"aria-label-open-metrics-explorer": "Open metrics explorer",
"placeholder-select-metric": "Select metric",
"tooltip-open-metrics-explorer": "Open metrics explorer"
},
"label-metric": "Metric",
"tooltip-metric": "Optional: returns a list of label values for the label name in the specified metric."
},
"metrics-modal": {
"aria-label-browse-metrics": "Browse metrics",
"currently-selected": "Currently selected: {{selected}}",
"metrics-pre-filtered": "These metrics have been pre-filtered by labels chosen in the label filters.",
"title-metrics-explorer": "Metrics explorer"
},
"nested-query": {
"label": {
"ignoring": "Ignoring",
"on": "On"
},
"operator": "Operator",
"tooltip-remove-match": "Remove match",
"vector-matches": "Vector matches"
},
"operation-editor": {
"not-found": "Operation {{id}} not found",
"title-remove": "Remove {{name}}"
},
"operation-header": {
"placeholder-replace-with": "Replace with",
"title-click-to-view-alternative-operations": "Click to view alternative operations",
"title-remove-operation": "Remove operation"
},
"operation-info-button": {
"title-click-to-show-description": "Click to show description",
"title-remove-operation": "Remove operation"
},
"operation-list": {
"operations": "Operations",
"placeholder-search": "Search",
"title-add-operation": "Add operation"
},
"operation-param-editor": {
"title-add": "Add {{name}}",
"title-remove": "Remove {{name}}"
},
"operation-utils": {
"getAggregationExplainer": {
"label-by": "Calculates {{aggregationName}} over dimensions while preserving {{labelWord}} {{labels}}.",
"label-default": "Calculates {{aggregationName}} over the dimensions.",
"label-without": "Calculates {{aggregationName}} over the dimensions {{labels}}. All other labels are preserved."
}
},
"prom-query-builder-options": {
"aria-label-exemplars": "Exemplars switch.",
"aria-label-format": "Format combobox",
"aria-label-lower-limit-parameter": "Min step text box, set lower limit for the step parameter",
"aria-label-select-resolution": "Select resolution",
"aria-label-type": "Type radio button group",
"format-options": {
"label-heatmap": "Heatmap",
"label-table": "Table",
"label-time-series": "Time series"
},
"label-exemplars": "Exemplars",
"label-format": "Format",
"label-min-step": "Min step",
"label-resolution": "Resolution",
"label-type": "Type",
"placeholder-auto": "auto",
"title-options": "Options",
"tooltip-min-step": "An additional lower limit for the step parameter of the Prometheus query and for the <2>{{interval}}</2> and <4>{{rateInterval}}</4> variables."
},
"prom-query-editor-selector": {
"body-syntax-error": "There is a syntax error, or the query structure cannot be visualized when switching to the builder mode. Parts of the query may be lost.",
"confirmText-continue": "Continue",
"kick-start-your-query": "Kick start your query",
"label-explain": "Explain",
"run-queries": "Run queries",
"title-parsing-error-switch-builder": "Parsing error: Switch to the builder mode?"
},
"prom-query-legend-editor": {
"aria-label-legend": "Legend combobox",
"label-legend": "Legend",
"placeholder-select-legend-mode": "Select legend mode",
"tooltip-legend": "Series name override or template. Ex. {{templateExample}} will be replaced with label value for {{labelName}}."
},
"query-builder-hints": {
"hint-details": "hint: {{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "Builder",
"label-code": "Code"
}
},
"query-pattern": {
"apply-query": "Apply query",
"aria-label-apply-query-starter-button": "apply query starter button",
"aria-label-back-button": "back button",
"aria-label-create-new-query-button": "create new query button",
"aria-label-raw-query": "{{patternName}} raw query",
"aria-label-use-this-query-button": "use this query button",
"back": "Back",
"create-new-query": "Create new query",
"use-this-query": "Use this query"
},
"query-patterns-modal": {
"aria-label-close-kick-start-your-query-modal": "close kick start your query modal",
"aria-label-kick-start-your-query-modal": "Kick start your query modal",
"aria-label-toggle-query-starter": "open and close {{patternType}} query starter card",
"close": "Close",
"description-kick-start-your-query": "Kick start your query by selecting one of these queries. You can then continue to complete your query.",
"label-toggle-query-starter": "{{patternType}} query starters",
"title-kick-start-your-query": "Kick start your query"
},
"raw-query": {
"aria-label-selector": "selector"
},
"results-table": {
"content-descriptive-type": "When creating a {{descriptiveType}}, Prometheus exposes multiple series with the type counter. ",
"description": "Description",
"message-expand-label-filters": "There are no metrics found. Try to expand your label filters.",
"message-expand-search": "There are no metrics found. Try to expand your search and filters.",
"message-no-metrics-found": "There are no metrics found in the data source.",
"name": "Name",
"type": "Type"
}
}
}
}

View File

@@ -1,492 +0,0 @@
{
"grafana-prometheus": {
"components": {
"annotation-query-editor": {
"annotation-data-load-error": "¡Error de carga de datos de anotación!",
"aria-label-lower-limit-parameter": "Establecer límite inferior para el parámetro de paso",
"label-min-step": "Paso mínimo",
"label-series-value-as-timestamp": "Valor de la serie como marca de tiempo",
"label-tags": "Etiquetas",
"label-text": "Texto",
"label-title": "Título",
"placeholder-auto": "auto",
"tooltip-either-pattern-example-instance-replaced-label": "Utiliza el nombre o un patrón. Por ejemplo, {{labelTemplate}} se reemplaza con el valor de etiqueta para la etiqueta {{labelName}}.",
"tooltip-min-step": "Un límite inferior adicional para el parámetro de paso de la consulta de Prometheus y para las variables <2>{{intervalVar}}</2> y <4>{{rateIntervalVar}}</4>.",
"tooltip-timestamp-milliseconds-series-value-seconds-multiply": "La unidad de la marca de tiempo es milisegundos. Si la unidad del valor de la serie es segundos, multiplica su vector de rango por 1000."
},
"get-query-type-options": {
"description": {
"instant-query-range": "Ejecutar una consulta instantánea y una consulta de rango"
},
"label": {
"both": "Ambas"
},
"range-options": {
"description": {
"query-range": "Ejecutar consulta en un rango de tiempo"
},
"label": {
"instant": "Instantánea",
"range": "Rango"
}
}
},
"label-selector": {
"aria-label-filter-expression-for-label": "Expresión de filtro para etiqueta",
"description-select-labels": "Una vez seleccionados los valores de las etiquetas, solo se muestran las posibles combinaciones de etiquetas.",
"select-labels-to-search-in": "2. Selecciona las etiquetas en las que buscar"
},
"metric-selector": {
"aria-label-filter-expression-for-metric": "Expresión de filtro para métrica",
"aria-label-limit-results-from-series-endpoint": "Limitar los resultados del punto de conexión de la serie",
"description-series-limit": "El límite se aplica a todas las métricas, etiquetas y valores. Deja el campo vacío para usar el límite predeterminado. Define el valor en 0 para desactivar el límite y obtenerlo todo (esto puede causar problemas de rendimiento).",
"label-select-metric": "Una vez seleccionada una métrica, solo se muestran las etiquetas posibles. Las etiquetas están limitadas por el límite de la serie a continuación.",
"select-a-metric": "1. Seleccionar una métrica",
"series-limit": "Límite de series"
},
"prom-cheat-sheet": {
"prom-ql-cheat-sheet": "Hoja de referencia de PromQL"
},
"prom-exemplar-field": {
"exemplars": "Ejemplos",
"tooltip-disable-query": "Deshabilitar consulta con ejemplos",
"tooltip-enable-query": "Habilitar consulta con ejemplos"
},
"prom-explore-extra-field": {
"aria-label-prometheus-extra-field": "Campo adicional de Prometheus",
"aria-label-query-type-field": "Campo de tipo de consulta",
"aria-label-step-field": "Campo de paso",
"min-step": "Paso mínimo",
"query-type": "Tipo de consulta",
"tooltip-units-builtin-variables-example-interval-rateinterval": "Aquí se pueden utilizar unidades de tiempo y variables integradas, por ejemplo: {{example1}}, {{example2}}, {{example3}}, {{example4}} {{example5}}, {{example6}}, {{example7}} (valor predeterminado si no se especifica ninguna unidad: {{default}})"
},
"prom-query-field": {
"placeholder-enter-a-prom-ql-query": "Introduce una consulta de PromQL..."
},
"prom-variable-query-editor": {
"aria-label-classic-query": "Consulta clásica",
"aria-label-metric-regex": "Expresión regular métrica",
"aria-label-metric-selector": "Selector de métricas",
"aria-label-prometheus-query": "Consulta de Prometheus",
"aria-label-query-type": "Tipo de consulta",
"aria-label-series-query": "Consulta de series",
"label-classic-query": "Consulta clásica",
"label-label": "Etiqueta",
"label-metric-regex": "Expresión regular métrica",
"label-query": "Consulta",
"label-query-type": "Tipo de consulta",
"label-series-query": "Consulta de series",
"placeholder-classic-query": "Consulta clásica",
"placeholder-metric-regex": "Expresión regular métrica",
"placeholder-prometheus-query": "Consulta de Prometheus",
"placeholder-select-query-type": "Seleccionar tipo de consulta",
"placeholder-series-query": "Consulta de series",
"returns-metrics-matching-specified-metric-regex": "Devuelve una lista de métricas que coinciden con la expresión regular de métrica especificada.",
"tooltip-classic-query": "La implementación original del editor de consultas de variables de Prometheus. Introduce una cadena con el tipo de consulta y los parámetros correctos como se describe en estos documentos. Por ejemplo, {{exampleQuery}}.",
"tooltip-label": "Devuelve una lista de valores de etiqueta para el nombre de etiqueta en todas las métricas, a menos que se especifique la métrica.",
"tooltip-metric-regex": "Devuelve una lista de nombres de etiquetas, filtrando opcionalmente por la expresión regular de métrica especificada.",
"tooltip-query": "Devuelve una lista de resultados de la consulta de Prometheus para la consulta. Esto puede incluir funciones de Prometheus, es decir, {{exampleQuery}}.",
"tooltip-query-type": "El plugin de fuente de datos de Prometheus proporciona los siguientes tipos de consulta para las variables de plantilla.",
"tooltip-series-query": "Introduzca una métrica con etiquetas, solo una métrica o solo etiquetas, es decir, {{example1}}, {{example2}} o {{example3}}. Devuelve una lista de series temporales asociadas con los datos introducidos."
},
"selector-actions": {
"aria-label-selector": "selector",
"aria-label-selector-clear-button": "Botón de borrado del selector",
"aria-label-use-selector-as-metrics-button": "Utilizar el selector como botón de métricas",
"aria-label-use-selector-for-query-button": "Utilizar el selector para el botón de consulta",
"aria-label-validate-submit-button": "Validar botón de envío",
"clear": "Borrar",
"resulting-selector": "4. Selector resultante",
"use-as-rate-query": "Utilizar como consulta de tasa",
"use-query": "Utilizar consulta",
"validate-selector": "Validar selector"
},
"value-selector": {
"aria-label-filter-expression-for-label-values": "Expresión de filtro para valores de etiqueta",
"aria-label-values-for": "Valores para {{labelKey}}",
"description-search-field-values-across-selected-labels": "Utiliza el campo de búsqueda para encontrar valores en las etiquetas seleccionadas.",
"select-multiple-values-for-your-labels": "3. Selecciona (varios) valores para sus etiquetas"
}
},
"configuration": {
"alerting-settings-overhaul": {
"label-allow-as-recording-rules-target": "Permitir como objetivo de las reglas de grabación",
"label-manage-alerts-via-alerting-ui": "Gestionar alertas a través de la interfaz de usuario de Alerting",
"title-alerting": "Alertas",
"tooltip-allow-as-recording-rules-target": "Permite que esta fuente de datos se seleccione como objetivo para escribir reglas de log.",
"tooltip-manage-alerts-via-alerting-ui": "Gestiona las reglas de alerta para esta fuente de datos. Para gestionar otros recursos de alerta, añade una fuente de datos de Alertmanager."
},
"config-editor": {
"browser-access-mode-error": "El modo de acceso a través del navegador en la fuente de datos de Prometheus ya no está disponible. Cambia al modo de acceso al servidor.",
"description-advanced-settings": "Los ajustes adicionales son ajustes opcionales que se pueden configurar para tener un mayor control sobre tu fuente de datos.",
"title-advanced-settings": "Ajustes avanzados",
"title-error": "Error"
},
"data-source-http-settings-overhaul": {
"tooltip-browser-access-mode": "Tu método de acceso es <1>Navegador</1>, esto significa que la URL debe ser accesible desde el navegador.",
"tooltip-http-url": "Especifica una URL HTTP completa (por ejemplo, {{exampleURL}})",
"tooltip-server-access-mode": "Tu método de acceso es <1>Servidor</1>, esto significa que la URL debe ser accesible desde el «backend» o servidor de Grafana."
},
"docs-tip": {
"visit-docs-for-more-details-here": "Consulta los documentos aquí para obtener más información."
},
"exemplar-setting": {
"label-data-source": "Fuente de datos",
"label-internal-link": "Enlace interno",
"label-label-name": "Nombre de la etiqueta",
"label-remove-exemplar-link": "Eliminar enlace de ejemplo",
"label-url": "URL",
"label-url-label": "Etiqueta de URL",
"placeholder-go-to-examplecom": "Ir a ejemplo.com",
"placeholder-httpsexamplecomvalueraw": "https://ejemplo.com/${__value.raw}",
"placeholder-trace-id": "traceID",
"title-remove-exemplar-link": "Eliminar enlace de ejemplo",
"tooltip-data-source": "La fuente de datos a la que se dirigirá el ejemplo.",
"tooltip-internal-link": "Habilita esta opción si tiene un enlace interno. Cuando está habilitado, indica el selector de fuente de datos. Selecciona el almacén de datos de trazas de backend para tus datos de ejemplo.",
"tooltip-label-name": "El nombre del campo en el objeto de etiquetas que debe utilizarse para obtener el traceID.",
"tooltip-url": "La URL del backend de traza al que el usuario iría para ver su traza",
"tooltip-url-label": "Utilícelo para anular la etiqueta del botón en el campo traceID del ejemplo."
},
"exemplars-settings": {
"add": "Añadir",
"no-exemplars-configurations": "No hay configuraciones de ejemplos",
"title-exemplars": "Ejemplos"
},
"prom-settings": {
"aria-label-default-editor": "Editor predeterminado (Código o Creador)",
"aria-label-prom-type-type": "Tipo {{promType}}",
"aria-label-prometheus-type": "Tipo de Prometheus",
"aria-label-select-http-method": "Seleccionar método HTTP",
"editor-options": {
"label-builder": "Constructor",
"label-code": "Código"
},
"label-cache-level": "Nivel de caché",
"label-custom-query-parameters": "Parámetros de consulta personalizados",
"label-default-editor": "Editor predeterminado",
"label-disable-metrics-lookup": "Deshabilitar la búsqueda de métricas",
"label-disable-recording-rules-beta": "Deshabilitar reglas de registro (beta)",
"label-http-method": "Método HTTP",
"label-incremental-querying-beta": "Consulta incremental (beta)",
"label-prom-type-version": "Versión de {{promType}}",
"label-prometheus-type": "Tipo de Prometheus",
"label-query-overlap-window": "Ventana de superposición de consultas",
"label-query-timeout": "Tiempo de espera de la consulta",
"label-scrape-interval": "Intervalo de recuperación de datos",
"label-series-limit": "Límite de series",
"label-use-series-endpoint": "Utilizar punto de conexión de la serie",
"more-info": "Para obtener más información sobre cómo configurar el tipo y la versión de Prometheus en las fuentes de datos, consulte la <2>documentación de aprovisionamiento</2>.",
"placeholder-example-maxsourceresolutionmtimeout": "Ejemplo: {{example}}",
"title-interval-behaviour": "Comportamiento del intervalo",
"title-other": "Otro",
"title-performance": "Rendimiento",
"title-query-editor": "Editor de consultas",
"tooltip-cache-level": "Establece el nivel de caché del navegador para las consultas del editor. Se recomienda una configuración de caché más alta para fuentes de datos de alta cardinalidad.",
"tooltip-custom-query-parameters": "Añada parámetros personalizados a la URL de la consulta de Prometheus. Por ejemplo, {{example1}}, {{example2}}, {{example3}} o{{example4}}. Deben concatenarse varios parámetros junto con {{concatenationChar}}.",
"tooltip-default-editor": "Establece la opción de editor predeterminada para todos los usuarios de esta fuente de datos.",
"tooltip-disable-metrics-lookup": "Al marcar esta opción, se desactivará el selector de métricas y la compatibilidad con métricas/etiquetas en el autocompletado del campo de consulta. Esto es útil si tienes problemas de rendimiento con instancias de Prometheus de mayor tamaño. ",
"tooltip-disable-recording-rules-beta": "Esta función deshabilitará las reglas de registro. Actívelo para mejorar el rendimiento del dashboard",
"tooltip-http-method": "Puedes utilizar el método HTTP POST o GET para consultar tu fuente de datos de Prometheus. POST es el método recomendado, ya que permite consultas de mayor tamaño. Cámbialo a GET si tienes una versión de Prometheus anterior a la 2.1 o si las solicitudes POST están restringidas en tu red.",
"tooltip-incremental-querying-beta": "Esta función cambiará el comportamiento predeterminado de las consultas relativas para solicitar siempre datos nuevos de la instancia de Prometheus, en lugar de almacenar en caché los resultados de las consultas y solo se solicitarán nuevos registros. Actívalo para disminuir la carga de la base de datos y de la red.",
"tooltip-prom-type-version": "Utiliza esta opción para establecer la versión de tu instancia {{promType}} si no se configura automáticamente.",
"tooltip-prometheus-type": "Define este valor en el tipo de tu base de datos de Prometheus, por ejemplo, Prometheus, Cortex, Mimir o Thanos. Si cambias este campo, se guardarán tus ajustes actuales. Ciertos tipos de Prometheus admiten o no admiten varias API. Por ejemplo, algunos tipos admiten la coincidencia de expresiones regulares para consultas de etiquetas para mejorar el rendimiento. Algunos tipos tienen una API para metadatos. Si configuraste valor incorrectamente, puedes experimentar un comportamiento extraño al consultar métricas y etiquetas. Consulta la documentación de Prometheus para asegurarte de introducir el tipo correcto.",
"tooltip-query-overlap-window": "Establece una duración como {{example1}}, {{example2}} o {{example3}}. El valor predeterminado es {{default}}. Esta duración se añadirá a la duración de cada solicitud incremental.",
"tooltip-query-timeout": "Establece el tiempo de espera de la consulta de Prometheus.",
"tooltip-scrape-interval": "Este intervalo es la frecuencia con la que Prometheus recupera datos de los objetivos. Establece este valor en el intervalo típico de recuperación de datos y evaluación definido en tu archivo de configuración de Prometheus. Si estableces un valor mayor que el intervalo de tu archivo de configuración de Prometheus, Grafana evaluará los datos de acuerdo con este intervalo y verás menos puntos de datos. El valor predeterminado es {{default}}.",
"tooltip-series-limit": "El límite se aplica a todos los recursos (métricas, etiquetas y valores) para ambos puntos finales (series y etiquetas). Deja el campo vacío para usar el límite predeterminado (40 000). Establece el valor en 0 para desactivar el límite y obtenerlo todo (esto puede causar problemas de rendimiento). El límite predeterminado es 40 000.",
"tooltip-use-series-endpoint": "Al marcar esta opción, se favorecerá el punto de conexión de la serie con el parámetro {{exampleParameter}} sobre el punto final de los valores de la etiqueta con el parámetro {{exampleParameter}}. Aunque el punto de conexión de los valores de etiqueta se considera más eficiente, algunos usuarios pueden preferir la serie porque tiene un método POST, mientras que el punto de los valores de etiqueta solo tiene un método GET."
}
},
"metrics-browser": {
"disabled-label": "(Deshabilitado)",
"enabled-label": "Navegador de métricas"
},
"prom-query-legend-editor": {
"get-legend-mode-options": {
"description-auto": "Solo incluye etiquetas únicas",
"description-custom": "Proporciona una plantilla de nombres",
"description-verbose": "Todos los nombres y valores de etiqueta",
"label-auto": "Automático",
"label-custom": "Personalizado",
"label-verbose": "Detallado"
}
},
"promql": {
"getAggregationOptions": {
"documentation-avg": "Calcular la media sobre las dimensiones",
"documentation-bottomk": "Elementos k más pequeños por valor de muestra",
"documentation-count": "Contar el número de elementos en el vector",
"documentation-count-values": "Contar el número de elementos con el mismo valor",
"documentation-group": "Todos los valores en el vector resultante son 1",
"documentation-max": "Seleccionar el máximo sobre las dimensiones",
"documentation-min": "Seleccionar mínimo sobre dimensiones",
"documentation-quantile": "Calcular el cuantil φ (0 ≤ φ ≤ 1) sobre las dimensiones",
"documentation-stddev": "Calcular la desviación estándar de la población sobre las dimensiones",
"documentation-stdvar": "Calcular la varianza estándar de la población sobre las dimensiones",
"documentation-sum": "Calcular la suma sobre las dimensiones",
"documentation-topk": "Elementos k más grandes por valor de muestra"
},
"getFunctions": {
"documentation-abs": "Devuelve el vector de entrada con todos los valores de muestra convertidos a su valor absoluto.",
"documentation-absent": "Devuelve un vector vacío si el vector que se le pasa tiene algún elemento y un vector de un elemento con el valor 1 si el vector que se le pasa no tiene elementos. Esto es útil para alertar cuando no existen series temporales para una combinación determinada de nombre y etiqueta de métrica.",
"documentation-absent-over-time": "Devuelve un vector vacío si el vector de rango que se le pasa tiene algún elemento y un vector de 1 elemento con valor 1 si el vector de rango que se le pasa no tiene elementos.",
"documentation-avg-over-time": "El valor medio de todos los puntos en el intervalo especificado.",
"documentation-ceil": "Redondea los valores de muestra de todos los elementos en «v» hacia arriba al número entero más cercano.",
"documentation-changes": "Para cada serie temporal de entrada, «changes(v range-vector)» devuelve el número de veces que su valor ha cambiado dentro del intervalo de tiempo proporcionado como un vector instantáneo.",
"documentation-clamp": "Fija los valores de muestra de todos los elementos en «v» para tener un límite inferior de «mín.» y un límite superior de «máx.».",
"documentation-clamp-max": "Limita los valores de muestra de todos los elementos en «v» para que tengan un límite superior de «máx.».",
"documentation-clamp-min": "Limita los valores de muestra de todos los elementos en «v» para que tengan un límite inferior de «mín.».",
"documentation-count-over-time": "El recuento de todos los valores en el intervalo especificado.",
"documentation-count-scalar": "Devuelve el número de elementos en un vector de serie temporal como un escalar. Esto contrasta con el operador de agregación «count()», que siempre devuelve un vector (uno vacío si el vector de entrada está vacío) y permite la agrupación por etiquetas a través de una cláusula «by».",
"documentation-day-of-month": "Devuelve el día del mes para cada una de las horas indicadas en UTC. Los valores devueltos van del 1 al 31.",
"documentation-day-of-week": "Devuelve el día de la semana para cada una de las horas indicadas en UTC. Los valores devueltos van del 0 al 6, donde 0 significa domingo, etc.",
"documentation-day-of-year": "Devuelve el día del año para cada una de las horas indicadas en UTC. Los valores devueltos van del 1 al 365 para los años no bisiestos y del 1 al 366 para los años bisiestos.",
"documentation-days-in-month": "Devuelve el número de días del mes para cada una de las horas indicadas en UTC. Los valores devueltos van del 28 al 31.",
"documentation-deg": "Convierte radianes a grados para todos los elementos en v",
"documentation-delta": "Calcula la diferencia entre el primer y el último valor de cada elemento de la serie temporal en un vector de rango «v», devolviendo un vector instantáneo con los deltas dados y las etiquetas equivalentes. El delta se extrapola para cubrir el rango de tiempo completo como se especifica en el selector de vector de rango, de modo que es posible obtener un resultado no entero incluso si los valores de muestra son todos enteros.",
"documentation-deriv": "Calcula la derivada por segundo de la serie temporal en un vector de rango «v», utilizando una regresión lineal simple.",
"documentation-double-exponential-smoothing": "Produce un valor suavizado para las series temporales en función del rango en «v». Cuanto menor sea el factor de suavizado «sf», más importancia se le dará a los datos antiguos. Cuanto mayor sea el factor de tendencia «tf», más tendencias se considerarán en los datos. Tanto «sf» como «tf» deben estar entre 0 y 1.",
"documentation-drop-common-labels": "Desecha todas las etiquetas que tienen el mismo nombre y valor en todas las series del vector de entrada.",
"documentation-exp": "Calcula la función exponencial para todos los elementos en «v».\nLos casos especiales son:\n* «Exp(+Inf) = +Inf» \n* «Exp(NaN) = NaN»",
"documentation-floor": "Redondea los valores de muestra de todos los elementos en «v» hacia abajo al número entero más cercano.",
"documentation-histogram-avg": "Devuelve la media aritmética de los valores observados almacenados en un histograma nativo. Las muestras que no son histogramas nativos se ignoran y no aparecen en el vector devuelto.",
"documentation-histogram-count": "Devuelve el recuento de observaciones almacenadas en un histograma nativo.",
"documentation-histogram-fraction": "Devuelve la fracción estimada de observaciones entre los valores inferior y superior proporcionados.",
"documentation-histogram-quantile": "Calcula el cuantil φ (0 ≤ φ ≤ 1) a partir de los cubos «b» de un histograma. Las muestras en «b» son los recuentos de observaciones en cada cubo. Cada muestra debe tener una etiqueta «le», donde el valor de la etiqueta denota el límite superior inclusivo del cubo. (Las muestras sin dicha etiqueta se ignoran silenciosamente). El tipo de métrica del histograma proporciona automáticamente series temporales con el sufijo «_bucket» y las etiquetas que correspondan.",
"documentation-histogram-stddev": "Devuelve la desviación estándar estimada de las observaciones en un histograma nativo, basada en la media geométrica de los segmentos en los que se encuentran las observaciones.",
"documentation-histogram-stdvar": "Devuelve la varianza estándar estimada de las observaciones en un histograma nativo.",
"documentation-histogram-sum": "Devuelve la suma de observaciones almacenadas en un histograma nativo.",
"documentation-holt-winters": "Renombrado como double_exponential_smoothing en Prometheus v3.x. Para versiones de Prometheus iguales y superiores a v3.0, utiliza double_exponential_smoothing. \n\nProduce un valor suavizado para las series temporales en función del rango en «v». Cuanto menor sea el factor de suavizado «sf», más importancia se le dará a los datos antiguos. Cuanto mayor sea el factor de tendencia «tf», más tendencias se considerarán en los datos. Tanto «sf» como «tf» deben estar entre 0 y 1.",
"documentation-hour": "Devuelve la hora del día para cada una de las horas indicadas en UTC. Los valores devueltos van de 0 a 23.",
"documentation-idelta": "Calcula la diferencia entre las dos últimas muestras en el vector de rango «v», devolviendo un vector instantáneo con los deltas dados y las etiquetas equivalentes.",
"documentation-increase": "Calcula el aumento en la serie temporal en el vector de rango. Las interrupciones en la monotonía (como los reinicios del contador debido a reinicios del objetivo) se ajustan automáticamente. El aumento se extrapola para cubrir todo el rango de tiempo especificado en el selector de vector de rango, de modo que es posible obtener un resultado no entero incluso si un contador aumenta solo en incrementos enteros.",
"documentation-info": "Devuelve los detalles y metadatos más recientes sobre un grupo de métricas, como sus etiquetas y valores actuales, sin realizar ningún cálculo",
"documentation-irate": "Calcula la tasa de aumento instantánea por segundo de la serie temporal en el vector de rango. Se basa en los dos últimos puntos de datos. Las interrupciones en la monotonía (como los reinicios del contador debido a reinicios del objetivo) se ajustan automáticamente.",
"documentation-label-join": "Para cada serie temporal en «v», une todos los valores de todas las «src_labels» usando «separator» y devuelve la serie temporal con la etiqueta «dst_label» que contiene el valor unido. Puede haber cualquier número de «src_labels» en esta función.",
"documentation-label-replace": "Para cada serie temporal en «v», «label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)» coincide con la expresión regular «regex» en la etiqueta «src_label». Si coincide, la serie temporal se devuelve con la etiqueta «dst_label» reemplazada por la expansión de «replacement». «$1» se sustituye por el primer subgrupo coincidente, «$2» por el segundo, etc. Si la expresión regular no coincide, entonces la serie temporal se devuelve sin cambios.",
"documentation-last-over-time": "El valor de punto más reciente en el intervalo especificado.",
"documentation-ln": "Calcula el logaritmo natural para todos los elementos en «v».\nLos casos especiales son:\n * «ln(+Inf) = +Inf»\n * «ln(0) = -Inf»\n * «ln(x < 0) = NaN»\n * «ln(NaN) = NaN»",
"documentation-log10": "Calcula el logaritmo decimal para todos los elementos en «v». Los casos especiales son equivalentes a los de «ln».",
"documentation-log2": "Calcula el logaritmo binario para todos los elementos en «v». Los casos especiales son equivalentes a los de «ln».",
"documentation-max-over-time": "El valor máximo de todos los puntos en el intervalo especificado.",
"documentation-min-over-time": "El valor mínimo de todos los puntos en el intervalo especificado.",
"documentation-minute": "Devuelve el minuto de la hora para cada una de las horas indicadas en UTC. Los valores devueltos van de 0 a 59.",
"documentation-month": "Devuelve el mes del año para cada una de las horas indicadas en UTC. Los valores devueltos van del 1 al 12, donde 1 significa enero, etc.",
"documentation-pi": "Devuelve pi",
"documentation-predict-linear": "Predice el valor de la serie temporal «t» segundos a partir de ahora, en función del vector de rango «v», utilizando una regresión lineal simple.",
"documentation-present-over-time": "El valor 1 para cualquier serie en el intervalo especificado.",
"documentation-quantile-over-time": "El cuantil φ (0 ≤ φ ≤ 1) de los valores en el intervalo especificado.",
"documentation-rad": "Convierte grados a radianes para todos los elementos en v",
"documentation-rate": "Calcula la tasa media de aumento por segundo de la serie temporal en el vector de rango. Las interrupciones en la monotonía (como los reinicios del contador debido a reinicios del objetivo) se ajustan automáticamente. Además, el cálculo se extrapola a los extremos del rango de tiempo, lo que permite tener en cuenta los raspados perdidos o una alineación imperfecta de los ciclos de raspado con el periodo de tiempo del rango.",
"documentation-resets": "Para cada serie temporal de entrada, «resets(v range-vector)» devuelve el número de reinicios del contador dentro del intervalo de tiempo proporcionado como un vector instantáneo. Cualquier disminución del valor entre dos muestras consecutivas se interpreta como un reinicio del contador.",
"documentation-round": "Redondea los valores de muestra de todos los elementos en «v» al número entero más cercano. Los empates se resuelven redondeando hacia arriba. El argumento opcional «to_nearest» permite especificar el múltiplo más cercano al que se deben redondear los valores de muestra. Este múltiplo también puede ser una fracción.",
"documentation-scalar": "Dado un vector de entrada de un solo elemento, «scalar(v instant-vector)» devuelve el valor de muestra de ese único elemento como un escalar. Si el vector de entrada no tiene exactamente un elemento, «scalar» devolverá «NaN».",
"documentation-sgn": "Devuelve un vector con todos los valores de muestra convertidos a su signo, definido de la siguiente manera: 1 si v es positivo, -1 si v es negativo y 0 si v es igual a cero.",
"documentation-sort": "Devuelve los elementos del vector ordenados por sus valores de muestra, en orden ascendente.",
"documentation-sort-desc": "Devuelve los elementos del vector ordenados por sus valores de muestra, en orden descendente.",
"documentation-sqrt": "Calcula la raíz cuadrada de todos los elementos en «v».",
"documentation-stddev-over-time": "La desviación estándar de la población de los valores en el intervalo especificado.",
"documentation-stdvar-over-time": "La varianza estándar de la población de los valores en el intervalo especificado.",
"documentation-sum-over-time": "La suma de todos los valores en el intervalo especificado.",
"documentation-time": "Devuelve el número de segundos desde el 1 de enero de 1970 UTC. Ten en cuenta que esto no devuelve la hora actual, sino la hora en la que se evaluará la expresión.",
"documentation-timestamp": "Devuelve la marca de tiempo de cada una de las muestras del vector dado como el número de segundos desde el 1 de enero de 1970 UTC.",
"documentation-vector": "Devuelve el escalar «s» como un vector sin etiquetas.",
"documentation-year": "Devuelve el año para cada una de las horas indicadas en UTC."
},
"getTrigonometricFunctions": {
"documentation-acos": "calcula el arcocoseno de todos los elementos en {{argument}}",
"documentation-acosh": "calcula el coseno hiperbólico inverso de todos los elementos en {{argument}}",
"documentation-asin": "calcula el arcoseno de todos los elementos en {{argument}}",
"documentation-asinh": "calcula el seno hiperbólico inverso de todos los elementos en {{argument}}",
"documentation-atan": "calcula el arcotangente de todos los elementos en {{argument}}",
"documentation-atanh": "calcula la tangente hiperbólica inversa de todos los elementos en {{argument}}",
"documentation-cos": "calcula el coseno de todos los elementos en {{argument}}",
"documentation-cosh": "calcula el coseno hiperbólico de todos los elementos en {{argument}}",
"documentation-sin": "calcula el seno de todos los elementos en {{argument}}",
"documentation-sinh": "calcula el seno hiperbólico de todos los elementos en {{argument}}",
"documentation-tan": "calcula la tangente de todos los elementos en {{argument}}",
"documentation-tanh": "calcula la tangente hiperbólica de todos los elementos en {{argument}}"
}
},
"querybuilder": {
"feedback-link": {
"give-feedback": "Enviar comentarios",
"title-give-feedback": "El explorador de métricas es nuevo. Háganos saber cómo podemos mejorarlo"
},
"get-collapsed-info": {
"exemplars": "Ejemplos: {{value}}",
"format": "Formato: {{value}}",
"legend": "Leyenda: {{value}}",
"step": "Paso: {{value}}",
"type": "Tipo: {{value}}"
},
"get-placeholders": {
"browse": "Buscar métricas por nombre",
"type": "Filtrar por tipo"
},
"get-prom-types": {
"description-counter": "Una métrica acumulativa que representa un solo contador que aumenta de forma monótona cuyo valor solo puede aumentar o restablecerse a cero al reiniciar.",
"description-gauge": "Una métrica que representa un solo valor numérico que puede subir y bajar arbitrariamente.",
"description-histogram": "Un histograma toma muestras de observaciones (normalmente cosas como la duración de las solicitudes o el tamaño de las respuestas) y las cuenta en cubos configurables.",
"description-native-histogram": "Los histogramas nativos se diferencian de los histogramas clásicos de Prometheus en varios aspectos: los límites de los cubos de los histogramas nativos se calculan mediante una fórmula que depende de la escala (resolución) del histograma nativo y no los define el usuario.",
"description-no-type": "Estas métricas no tienen un tipo definido en los metadatos.",
"description-summary": "Resume las observaciones de las muestras (normalmente aspectos como la duración de las solicitudes y el tamaño de las respuestas) y puede calcular cuantiles configurables en una ventana de tiempo deslizante.",
"description-unknown": "A estas métricas se les ha dado el tipo «desconocido» en los metadatos.",
"label-counter": "Contador",
"label-gauge": "Medidor",
"label-histogram": "Histograma",
"label-native-histogram": "Histograma nativo",
"label-no-type": "Ningún tipo",
"label-summary": "Resumen",
"label-unknown": "Desconocido"
},
"handle-function": {
"text": {
"query-parsing-is-ambiguous": "El análisis de la consulta es ambiguo."
}
},
"label-filter-item": {
"aria-label-remove": "Eliminar {{name}}",
"placeholder-select-label": "Seleccionar etiqueta",
"placeholder-select-value": "Seleccionar valor"
},
"label-filters": {
"label-filters": "Filtros de etiquetas",
"label-label-filters": "Filtros de etiquetas",
"tooltip-label-filters": "Opcional: se utiliza para filtrar la selección de métricas para este tipo de consulta."
},
"label-param-editor": {
"loadingMessage-loading-labels": "Cargando etiquetas",
"noOptionsMessage-no-labels-found": "No se ha encontrado ninguna etiqueta"
},
"metric-combobox": {
"async-select": {
"aria-label-open-metrics-explorer": "Abrir el explorador de métricas",
"placeholder-select-metric": "Seleccionar métrica",
"tooltip-open-metrics-explorer": "Abrir el explorador de métricas"
},
"label-metric": "Métrica",
"tooltip-metric": "Opcional: devuelve una lista de valores de etiqueta para el nombre de etiqueta en la métrica especificada."
},
"metrics-modal": {
"aria-label-browse-metrics": "Examinar métricas",
"currently-selected": "Seleccionada actualmente: {{selected}}",
"metrics-pre-filtered": "Estas métricas se han filtrado previamente por las etiquetas elegidas en los filtros de etiquetas.",
"title-metrics-explorer": "Explorador de métricas"
},
"nested-query": {
"label": {
"ignoring": "Ignorando",
"on": "Activada"
},
"operator": "Operador",
"tooltip-remove-match": "Eliminar coincidencia",
"vector-matches": "Coincidencias de vector"
},
"operation-editor": {
"not-found": "Operación {{id}} no encontrada",
"title-remove": "Eliminar {{name}}"
},
"operation-header": {
"placeholder-replace-with": "Reemplazar con",
"title-click-to-view-alternative-operations": "Haz clic para ver operaciones alternativas",
"title-remove-operation": "Eliminar operación"
},
"operation-info-button": {
"title-click-to-show-description": "Haz clic para mostrar la descripción",
"title-remove-operation": "Eliminar operación"
},
"operation-list": {
"operations": "Operaciones",
"placeholder-search": "Buscar",
"title-add-operation": "Añadir operación"
},
"operation-param-editor": {
"title-add": "Añadir {{name}}",
"title-remove": "Eliminar {{name}}"
},
"operation-utils": {
"getAggregationExplainer": {
"label-by": "Calcula {{aggregationName}} sobre las dimensiones a la vez que conserva {{labelWord}} {{labels}}.",
"label-default": "Calcula {{aggregationName}} sobre las dimensiones.",
"label-without": "Calcula {{aggregationName}} sobre las dimensiones {{labels}}. Todas las demás etiquetas se conservan."
}
},
"prom-query-builder-options": {
"aria-label-exemplars": "Cambio de ejemplos.",
"aria-label-format": "Formatear cuadro combinado",
"aria-label-lower-limit-parameter": "Cuadro de texto de paso mínimo, establecer límite inferior para el parámetro de paso",
"aria-label-select-resolution": "Seleccionar resolución",
"aria-label-type": "Grupo de botones de opción de tipo",
"format-options": {
"label-heatmap": "Mapa de calor",
"label-table": "Tabla",
"label-time-series": "Serie temporal"
},
"label-exemplars": "Ejemplos",
"label-format": "Formato",
"label-min-step": "Paso mínimo",
"label-resolution": "Resolución",
"label-type": "Tipo",
"placeholder-auto": "auto",
"title-options": "Opciones",
"tooltip-min-step": "Un límite inferior adicional para el parámetro de paso de la consulta de Prometheus y para las variables <2>{{interval}}</2> y <4>{{rateInterval}}</4>."
},
"prom-query-editor-selector": {
"body-syntax-error": "Hay un error de sintaxis o la estructura de la consulta no se puede visualizar al cambiar al modo constructor. Es posible que se pierdan partes de la consulta.",
"confirmText-continue": "Continuar",
"kick-start-your-query": "Inicie su consulta",
"label-explain": "Explicación",
"run-queries": "Ejecutar consultas",
"title-parsing-error-switch-builder": "Error de análisis: ¿cambiar al modo Creador?"
},
"prom-query-legend-editor": {
"aria-label-legend": "Cuadro combinado de leyenda",
"label-legend": "Leyenda",
"placeholder-select-legend-mode": "Seleccionar modo de leyenda",
"tooltip-legend": "Anulación o plantilla del nombre de la serie. Por ejemplo, {{templateExample}} se reemplazará con el valor de etiqueta para {{labelName}}."
},
"query-builder-hints": {
"hint-details": "pista: {{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "Constructor",
"label-code": "Código"
}
},
"query-pattern": {
"apply-query": "Aplicar consulta",
"aria-label-apply-query-starter-button": "botón de inicio de aplicación de consulta",
"aria-label-back-button": "botón para volver",
"aria-label-create-new-query-button": "botón para crear una nueva consulta",
"aria-label-raw-query": "Consulta sin procesar {{patternName}}",
"aria-label-use-this-query-button": "utilizar este botón de consulta",
"back": "Atrás",
"create-new-query": "Crear una nueva consulta",
"use-this-query": "Utilizar esta consulta"
},
"query-patterns-modal": {
"aria-label-close-kick-start-your-query-modal": "cerrar iniciar tu modo de consulta",
"aria-label-kick-start-your-query-modal": "Inicie su modal de consulta",
"aria-label-toggle-query-starter": "abrir y cerrar tarjeta de iniciador de consultas {{patternType}}",
"close": "Cerrar",
"description-kick-start-your-query": "Inicia tu consulta seleccionando una de estas consultas. A continuación, puedes continuar para completar tu consulta.",
"label-toggle-query-starter": "iniciadores de consultas {{patternType}}",
"title-kick-start-your-query": "Inicie su consulta"
},
"raw-query": {
"aria-label-selector": "selector"
},
"results-table": {
"content-descriptive-type": "Al crear un {{descriptiveType}}, Prometheus expone varias series con el contador de tipos. ",
"description": "Descripción",
"message-expand-label-filters": "No se han encontrado métricas. Intenta ampliar los filtros de etiquetas.",
"message-expand-search": "No se han encontrado métricas. Intenta ampliar la búsqueda y los filtros.",
"message-no-metrics-found": "No se han encontrado métricas en la fuente de datos.",
"name": "Nombre",
"type": "Tipo"
}
}
}
}

View File

@@ -1,492 +0,0 @@
{
"grafana-prometheus": {
"components": {
"annotation-query-editor": {
"annotation-data-load-error": "Erreur de chargement des données dannotation !",
"aria-label-lower-limit-parameter": "Définir la limite inférieure pour le paramètre détape",
"label-min-step": "Étape minimum",
"label-series-value-as-timestamp": "Valeur de la série en tant quhorodatage",
"label-tags": "Balises",
"label-text": "Texte",
"label-title": "Titre",
"placeholder-auto": "auto",
"tooltip-either-pattern-example-instance-replaced-label": "Utilisez le nom ou un modèle. Par exemple, {{labelTemplate}} est remplacé par la valeur de létiquette pour létiquette {{labelName}}.",
"tooltip-min-step": "Une limite inférieure supplémentaire pour le paramètre détape de la requête Prometheus et pour les variables <2>{{intervalVar}}</2> et <4>{{rateIntervalVar}}</4>.",
"tooltip-timestamp-milliseconds-series-value-seconds-multiply": "Lunité de lhorodatage est la milliseconde. Si lunité de la valeur de la série est en secondes, multipliez son vecteur de plage par 1 000."
},
"get-query-type-options": {
"description": {
"instant-query-range": "Exécuter une requête instantanée et une requête de plage"
},
"label": {
"both": "Les deux"
},
"range-options": {
"description": {
"query-range": "Exécuter la requête sur une plage de temps"
},
"label": {
"instant": "Instantané",
"range": "Plage"
}
}
},
"label-selector": {
"aria-label-filter-expression-for-label": "Filtrer lexpression pour létiquette",
"description-select-labels": "Une fois les valeurs détiquette sélectionnées, seules les combinaisons détiquettes possibles sont affichées.",
"select-labels-to-search-in": "2. Sélectionner les étiquettes à rechercher"
},
"metric-selector": {
"aria-label-filter-expression-for-metric": "Filtrer lexpression pour la métrique",
"aria-label-limit-results-from-series-endpoint": "Limiter les résultats du point de terminaison de la série",
"description-series-limit": "La limite sapplique à toutes les métriques, étiquettes et valeurs. Laissez le champ vide pour utiliser la limite par défaut. Définissez la valeur sur 0 pour désactiver la limite et tout récupérer. Cela peut entraîner des problèmes de performances.",
"label-select-metric": "Une fois quune métrique est sélectionnée, seules les étiquettes possibles sont affichées. Les étiquettes sont limitées par la limite de séries ci-dessous.",
"select-a-metric": "1. Sélectionner une métrique",
"series-limit": "Limite de séries"
},
"prom-cheat-sheet": {
"prom-ql-cheat-sheet": "Aide-mémoire PromQL"
},
"prom-exemplar-field": {
"exemplars": "Exemples",
"tooltip-disable-query": "Désactiver la requête avec des exemples",
"tooltip-enable-query": "Activer la requête avec des exemples"
},
"prom-explore-extra-field": {
"aria-label-prometheus-extra-field": "Champ supplémentaire Prometheus",
"aria-label-query-type-field": "Champ de type de requête",
"aria-label-step-field": "Champ détape",
"min-step": "Étape minimum",
"query-type": "Type de requête",
"tooltip-units-builtin-variables-example-interval-rateinterval": "Les unités de temps et les variables intégrées peuvent être utilisées ici, par exemple : {{example1}}, {{example2}}, {{example3}} {{example4}}, {{example5}}, {{example6}}, {{example7}} (par défaut si aucune unité nest spécifiée : {{default}})"
},
"prom-query-field": {
"placeholder-enter-a-prom-ql-query": "Saisissez une requête PromQL…"
},
"prom-variable-query-editor": {
"aria-label-classic-query": "Requête classique",
"aria-label-metric-regex": "Expression régulière de métrique",
"aria-label-metric-selector": "Sélecteur de métrique",
"aria-label-prometheus-query": "Requête Prometheus",
"aria-label-query-type": "Type de requête",
"aria-label-series-query": "Requête de séries",
"label-classic-query": "Requête classique",
"label-label": "Étiquette",
"label-metric-regex": "Expression régulière de métrique",
"label-query": "Requête",
"label-query-type": "Type de requête",
"label-series-query": "Requête de séries",
"placeholder-classic-query": "Requête classique",
"placeholder-metric-regex": "Expression régulière de métrique",
"placeholder-prometheus-query": "Requête Prometheus",
"placeholder-select-query-type": "Sélectionner un type de requête",
"placeholder-series-query": "Requête de séries",
"returns-metrics-matching-specified-metric-regex": "Renvoie une liste de métriques correspondant à lexpression régulière de métrique spécifiée.",
"tooltip-classic-query": "Limplémentation dorigine de léditeur de requêtes à variables Prometheus. Saisissez une chaîne de caractères avec le type de requête et les paramètres appropriés comme décrit dans la documentation. Par exemple, {{exampleQuery}}.",
"tooltip-label": "Renvoie une liste de valeurs détiquette pour le nom détiquette dans toutes les métriques, sauf si la métrique est spécifiée.",
"tooltip-metric-regex": "Renvoie une liste de noms détiquettes, en filtrant éventuellement par expression régulière de métrique spécifiée.",
"tooltip-query": "Renvoie une liste de résultats de requête Prometheus pour la requête. Cela peut inclure des fonctions Prometheus, par exemple {{exampleQuery}}.",
"tooltip-query-type": "Le plugin de source de données Prometheus fournit les types de requêtes suivants pour les variables de modèle.",
"tooltip-series-query": "Saisissez une métrique avec des étiquettes, uniquement une métrique ou uniquement des étiquettes, cest-à-dire {{example1}}, {{example2}} ou {{example3}}. Renvoie une liste de séries chronologiques associées aux données saisies."
},
"selector-actions": {
"aria-label-selector": "sélecteur",
"aria-label-selector-clear-button": "Bouton Effacer le sélecteur",
"aria-label-use-selector-as-metrics-button": "Bouton Utiliser le sélecteur comme métrique",
"aria-label-use-selector-for-query-button": "Bouton Utiliser le sélecteur pour la requête",
"aria-label-validate-submit-button": "Bouton Valider lenvoi",
"clear": "Effacer",
"resulting-selector": "4. Sélecteur du résultat",
"use-as-rate-query": "Utiliser comme requête de taux",
"use-query": "Utiliser la requête",
"validate-selector": "Valider le sélecteur"
},
"value-selector": {
"aria-label-filter-expression-for-label-values": "Filtrer lexpression pour les valeurs de létiquette",
"aria-label-values-for": "Valeurs pour {{labelKey}}",
"description-search-field-values-across-selected-labels": "Utilisez le champ de recherche pour trouver des valeurs parmi les étiquettes sélectionnées.",
"select-multiple-values-for-your-labels": "3. Sélectionner (plusieurs) valeurs pour vos étiquettes"
}
},
"configuration": {
"alerting-settings-overhaul": {
"label-allow-as-recording-rules-target": "Autoriser comme cible des règles denregistrement",
"label-manage-alerts-via-alerting-ui": "Gérer les alertes via linterface utilisateur dalerte",
"title-alerting": "Alerte",
"tooltip-allow-as-recording-rules-target": "Permettez à cette source de données dêtre sélectionnée comme cible pour lécriture des règles denregistrement.",
"tooltip-manage-alerts-via-alerting-ui": "Gérez les règles d'alerte pour cette source de données. Pour gérer d'autres ressources d'alerte, ajoutez une source de données Alertmanager."
},
"config-editor": {
"browser-access-mode-error": "Le mode daccès via le navigateur à la source de données Prometheus nest plus disponible. Passez en mode daccès au serveur.",
"description-advanced-settings": "Les paramètres supplémentaires sont des options facultatives que vous pouvez configurer pour un meilleur contrôle de votre source de données.",
"title-advanced-settings": "Paramètres avancés",
"title-error": "Erreur"
},
"data-source-http-settings-overhaul": {
"tooltip-browser-access-mode": "Votre méthode d'accès est <1>Navigateur</1> ; cela signifie que l'URL doit être accessible à partir du navigateur.",
"tooltip-http-url": "Spécifiez une URL HTTP complète (par exemple : {{exampleURL}})",
"tooltip-server-access-mode": "Votre méthode d'accès est <1>Serveur</1> ; cela signifie que l'URL doit être accessible à partir du serveur/back-end Grafana."
},
"docs-tip": {
"visit-docs-for-more-details-here": "Consultez les documents pour en savoir plus ici."
},
"exemplar-setting": {
"label-data-source": "Source de données",
"label-internal-link": "Lien interne",
"label-label-name": "Nom de létiquette",
"label-remove-exemplar-link": "Supprimer le lien dexemple",
"label-url": "URL",
"label-url-label": "Étiquette pour lURL",
"placeholder-go-to-examplecom": "Accéder à example.com",
"placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}",
"placeholder-trace-id": "traceID",
"title-remove-exemplar-link": "Supprimer le lien dexemple",
"tooltip-data-source": "La source de données vers laquelle lexemple va naviguer.",
"tooltip-internal-link": "Activez cette option si vous avez un lien interne. Lorsque cette option est activée, le sélecteur de source de données est affiché. Sélectionnez le magasin de données de traçage backend pour vos données dexemple.",
"tooltip-label-name": "Le nom du champ dans lobjet étiquette qui doit être utilisé pour obtenir le traceID.",
"tooltip-url": "LURL du backend de trace que lutilisateur consulterait pour voir sa trace",
"tooltip-url-label": "À utiliser pour remplacer létiquette du bouton sur le champ traceID dexemple."
},
"exemplars-settings": {
"add": "Ajouter",
"no-exemplars-configurations": "Aucune configuration dexemples",
"title-exemplars": "Exemples"
},
"prom-settings": {
"aria-label-default-editor": "Éditeur par défaut (Code ou Créateur)",
"aria-label-prom-type-type": "Type {{promType}}",
"aria-label-prometheus-type": "Type Prometheus",
"aria-label-select-http-method": "Sélectionner la méthode HTTP",
"editor-options": {
"label-builder": "Builder",
"label-code": "Code"
},
"label-cache-level": "Niveau du cache",
"label-custom-query-parameters": "Paramètres de requête personnalisés",
"label-default-editor": "Éditeur par défaut",
"label-disable-metrics-lookup": "Désactiver la recherche de métriques",
"label-disable-recording-rules-beta": "Désactiver les règles denregistrement (bêta)",
"label-http-method": "Méthode HTTP",
"label-incremental-querying-beta": "Requête incrémentale (bêta)",
"label-prom-type-version": "Version {{promType}}",
"label-prometheus-type": "Type Prometheus",
"label-query-overlap-window": "Fenêtre de chevauchement de requête",
"label-query-timeout": "Délai dexpiration de la requête",
"label-scrape-interval": "Intervalle de scraping",
"label-series-limit": "Limite de séries",
"label-use-series-endpoint": "Utiliser le point de terminaison de la série",
"more-info": "Pour en savoir plus sur la configuration du type et de la version de Prometheus dans les sources de données, consultez la <2>documentation sur le provisionnement</2>.",
"placeholder-example-maxsourceresolutionmtimeout": "Exemple : {{example}}",
"title-interval-behaviour": "Comportement de lintervalle",
"title-other": "Autre",
"title-performance": "Performance",
"title-query-editor": "Éditeur de requêtes",
"tooltip-cache-level": "Définit le niveau de mise en cache du navigateur pour les requêtes de léditeur. Des paramètres de cache plus élevés sont recommandés pour les sources de données à cardinalité élevée.",
"tooltip-custom-query-parameters": "Ajoutez des paramètres personnalisés à lURL de la requête Prometheus. Par exemple {{example1}}, {{example2}}, {{example3}}, ou {{example4}}. Plusieurs paramètres doivent être concaténés avec {{concatenationChar}}.",
"tooltip-default-editor": "Définir loption déditeur par défaut pour tous les utilisateurs de cette source de données.",
"tooltip-disable-metrics-lookup": "Cocher cette option désactivera le sélecteur de métriques et la prise en charge des métriques/étiquettes dans la saisie semi-automatique du champ de requête. Cela vous aide si vous avez des problèmes de performances avec des instances Prometheus plus grandes. ",
"tooltip-disable-recording-rules-beta": "Cette fonctionnalité désactivera les règles denregistrement. Activez cette option pour améliorer les performances du tableau de bord",
"tooltip-http-method": "Vous pouvez utiliser la méthode POST ou GET HTTP pour interroger votre source de données Prometheus. POST est la méthode recommandée car elle permet des requêtes plus volumineuses. Changez ceci en GET si vous avez une version Prometheus antérieure à la version 2.1 ou si les demandes POST sont restreintes dans votre réseau.",
"tooltip-incremental-querying-beta": "Cette fonctionnalité modifiera le comportement par défaut des requêtes relatives pour toujours demander des données récentes à linstance Prometheus. Au lieu de cela, les résultats des requêtes seront mis en cache et seuls les nouveaux enregistrements seront demandés. Activez cette option pour réduire la charge de la base de données et du réseau.",
"tooltip-prom-type-version": "Utilisez cette option pour définir la version de votre instance {{promType}} si elle nest pas configurée automatiquement.",
"tooltip-prometheus-type": "Définissez-la sur le type de votre base de données Prometheus, par exemple Prometheus, Cortex, Mimir ou Thanos. La modification de ce champ enregistrera vos paramètres actuels. Certains types de Prometheus prennent en charge ou non diverses API. Par exemple, certains types prennent en charge la correspondance dexpressions régulières pour les requêtes détiquettes afin daméliorer les performances. Certains types ont une API pour les métadonnées. Si vous définissez cette option de manière incorrecte, vous risquez dobserver un comportement inhabituel lors de la requête de métriques et détiquettes. Veuillez consulter votre documentation Prometheus pour vous assurer que vous saisissez le bon type.",
"tooltip-query-overlap-window": "Définissez une durée comme {{example1}} ou {{example2}} ou {{example3}}. Par défaut : {{default}}. Cette durée sera ajoutée à la durée de chaque demande incrémentielle.",
"tooltip-query-timeout": "Définissez le délai dexpiration de la requête Prometheus.",
"tooltip-scrape-interval": "Cet intervalle correspond à la fréquence à laquelle Prometheus récupère les cibles. Définissez-la sur lintervalle de scraping et dévaluation typique configuré dans votre fichier de configuration Prometheus. Si vous définissez cette valeur sur une valeur supérieure à lintervalle de votre fichier de configuration Prometheus, Grafana évaluera les données en fonction de cet intervalle et vous verrez moins de points de données. La valeur par défaut est {{default}}.",
"tooltip-series-limit": "La limite sapplique à toutes les ressources (métriques, étiquettes et valeurs) pour les deux points de terminaison (séries et étiquettes). Laissez le champ vide pour utiliser la limite par défaut (40 000). Définissez la valeur sur 0 pour désactiver la limite et tout récupérer. Cela peut nuire aux performances. La limite par défaut est de 40 000.",
"tooltip-use-series-endpoint": "Cocher cette option favorisera le point de terminaison de la série avec le paramètre {{exampleParameter}} par rapport au point de terminaison des valeurs détiquette avec le paramètre {{exampleParameter}}. Bien que le point de terminaison des valeurs détiquette soit considéré comme plus performant, certains utilisateurs peuvent préférer la série, car elle dispose dune méthode POST tandis que le point de terminaison des valeurs détiquette ne dispose que dune méthode GET."
}
},
"metrics-browser": {
"disabled-label": "(Désactivé)",
"enabled-label": "Navigateur de métriques"
},
"prom-query-legend-editor": {
"get-legend-mode-options": {
"description-auto": "Inclut uniquement les étiquettes uniques",
"description-custom": "Fournir un modèle de nommage",
"description-verbose": "Tous les noms et valeurs détiquettes",
"label-auto": "Auto",
"label-custom": "Personnalisée",
"label-verbose": "Verbeux"
}
},
"promql": {
"getAggregationOptions": {
"documentation-avg": "Calculer la moyenne par dimension",
"documentation-bottomk": "Les plus petits éléments k selon la valeur de léchantillon",
"documentation-count": "Compter le nombre déléments dans le vecteur",
"documentation-count-values": "Compter le nombre déléments avec la même valeur",
"documentation-group": "Toutes les valeurs du vecteur résultant sont égales à 1",
"documentation-max": "Sélectionner la valeur maximale par dimension",
"documentation-min": "Sélectionner la valeur minimale par dimension",
"documentation-quantile": "Calculer le quantile φ (0  φ  1) par dimension",
"documentation-stddev": "Calculer lécart-type de la population par dimension",
"documentation-stdvar": "Calculer la variance-type de la population par dimension",
"documentation-sum": "Calculer la somme par dimension",
"documentation-topk": "Les plus grands éléments k selon la valeur de léchantillon"
},
"getFunctions": {
"documentation-abs": "Renvoie le vecteur dentrée avec toutes les valeurs converties en valeurs absolues.",
"documentation-absent": "Renvoie un vecteur vide si le vecteur passé en paramètre contient des éléments et un vecteur à un élément contenant la valeur 1 sil est vide. Utile pour déclencher des alertes lorsquaucune série chronologique nexiste pour une combinaison donnée de nom de métrique et détiquette.",
"documentation-absent-over-time": "Renvoie un vecteur vide si le vecteur de plage passé en paramètre contient des éléments et un vecteur à un élément contenant la valeur 1 sil est vide.",
"documentation-avg-over-time": "La valeur moyenne de tous les points dans lintervalle spécifié.",
"documentation-ceil": "Arrondit à lentier supérieur les valeurs déchantillon de tous les éléments dans « v ».",
"documentation-changes": "Pour chaque série chronologique dentrée, « changes(v range-vector) » renvoie le nombre de fois où sa valeur a changé dans la période spécifiée, sous forme de vecteur instantané.",
"documentation-clamp": "Saturation des valeurs déchantillon des éléments de « v » : applique une borne inférieure « min. » et une borne supérieure « max. ».",
"documentation-clamp-max": "Saturation des valeurs déchantillon des éléments de « v » : applique une borne supérieure « max. ».",
"documentation-clamp-min": "Saturation des valeurs déchantillon des éléments de « v » : applique une borne inférieure « min. ».",
"documentation-count-over-time": "Le nombre total de valeurs dans lintervalle spécifié.",
"documentation-count-scalar": "Renvoie le nombre déléments dans un vecteur de séries chronologiques sous forme scalaire. Contrairement à lopérateur dagrégation « count() », qui renvoie toujours un vecteur (même vide) et permet un regroupement via « by ».",
"documentation-day-of-month": "Renvoie le jour du mois pour chaque horodatage fourni (en UTC). Les valeurs renvoyées vont de 1 à 31.",
"documentation-day-of-week": "Renvoie le jour de la semaine pour chaque horodatage fourni (en UTC). Les valeurs renvoyées vont de 0 (dimanche) à 6 (samedi).",
"documentation-day-of-year": "Renvoie le jour de lannée pour chaque horodatage fourni (en UTC). Les valeurs renvoyées vont de 1 à 365 pour les années non bissextiles et de 1 à 366 pour les années bissextiles.",
"documentation-days-in-month": "Renvoie le nombre de jours dans le mois pour chaque horodatage fourni (en UTC). Les valeurs renvoyées vont de 28 à 31.",
"documentation-deg": "Convertit des radians en degrés pour tous les éléments dans v",
"documentation-delta": "Calcule la différence entre la première et la dernière valeur de chaque élément dune série chronologique dans un vecteur de plage « v » et renvoie un vecteur instantané avec les écarts et les étiquettes correspondantes. Lécart est extrapolé sur toute la plage de temps, ce qui peut produire des résultats non entiers même avec des valeurs déchantillon entières.",
"documentation-deriv": "Calcule la dérivée par seconde dune série chronologique dans un vecteur de plage « v », à laide dune régression linéaire simple.",
"documentation-double-exponential-smoothing": "Produit une valeur lissée pour une série chronologique à partir de la plage « v ». Plus le facteur de lissage « sf » est bas, plus limportance est donnée aux anciennes données. Plus le facteur de tendance « tf » est élevé, plus les tendances sont prises en compte. Les valeurs de « sf » et de « tf » doivent être comprises entre 0 et 1.",
"documentation-drop-common-labels": "Supprime toutes les étiquettes dont les noms et les valeurs sont identiques dans toutes les séries du vecteur dentrée.",
"documentation-exp": "Calcule la fonction exponentielle de tous les éléments de « v ».\nCas particuliers :\n* « Exp(+Inf) = +Inf » \n* « Exp(NaN) = NaN »",
"documentation-floor": "Arrondit à lentier inférieur les valeurs déchantillon de tous les éléments dans « v ».",
"documentation-histogram-avg": "Renvoie la moyenne arithmétique des valeurs observées dans un histogramme natif. Les échantillons non natifs sont ignorés et exclus du vecteur retourné.",
"documentation-histogram-count": "Renvoie le nombre dobservations enregistrées dans un histogramme natif.",
"documentation-histogram-fraction": "Renvoie la fraction estimée des observations comprises entre les valeurs inférieure et supérieure fournies.",
"documentation-histogram-quantile": "Calcule le quantile φ (0  φ  1) à partir des buckets « b » dun histogramme. Les échantillons dans « b » correspondent aux comptes dobservations dans chaque bucket. Chaque échantillon doit comporter une étiquette « le » dont la valeur indique la borne supérieure (incluse) du bucket. (Les échantillons ne comportant pas cette étiquette sont ignorés sans avertissement.) Le type de métrique histogramme fournit automatiquement des séries chronologiques avec le suffixe « _bucket » et les étiquettes appropriées.",
"documentation-histogram-stddev": "Renvoie lécart-type estimé des observations dans un histogramme natif, calculé à partir de la moyenne géométrique des buckets dans lesquels se trouvent les observations.",
"documentation-histogram-stdvar": "Renvoie la variance-type estimée des observations dans un histogramme natif.",
"documentation-histogram-sum": "Renvoie la somme des observations enregistrées dans un histogramme natif.",
"documentation-holt-winters": "Renommée en double_exponential_smoothing dans Prometheus v3.x. Pour les versions de Prometheus égales ou supérieures à la v3.0, merci dutiliser double_exponential_smoothing. \n\nGénère une valeur lissée pour une série chronologique en fonction de lintervalle défini dans « v ». Plus le facteur de lissage « sf » est faible, plus limportance est donnée aux données anciennes. Plus le facteur de tendance « tf » est élevé, plus les tendances des données sont prises en compte. Les valeurs de « sf » et de « tf » doivent être comprises entre 0 et 1.",
"documentation-hour": "Renvoie lheure de la journée pour chaque horodatage fourni (en UTC). Les valeurs renvoyées vont de 0 à 23.",
"documentation-idelta": "Calcule la différence entre les deux derniers échantillons dans le vecteur de plage « v » et renvoie un vecteur instantané contenant les deltas et les étiquettes correspondantes.",
"documentation-increase": "Calcule laugmentation dune série chronologique sur une période donnée. Les ruptures de monotonie (comme les remises à zéro du compteur liées à un redémarrage de la cible) sont automatiquement corrigées. Laugmentation est extrapolée sur lensemble de lintervalle de temps spécifié dans le sélecteur de plage, ce qui permet dobtenir un résultat non entier, même si le compteur naugmente que par valeurs entières.",
"documentation-info": "Renvoie les dernières métadonnées disponibles sur un groupe de métriques, comme leurs étiquettes et valeurs actuelles, sans effectuer de calcul",
"documentation-irate": "Calcule le taux instantané daugmentation par seconde dune série chronologique sur lintervalle donné. Ce calcul repose sur les deux derniers points de données. Les ruptures de monotonie (comme les remises à zéro du compteur dues à un redémarrage de la cible) sont automatiquement corrigées.",
"documentation-label-join": "Pour chaque série chronologique dans « v », concatène toutes les valeurs des étiquettes « src_labels » en utilisant un « séparateur », puis renvoie la série avec une nouvelle étiquette « dst_label » contenant cette valeur concaténée. Cette fonction peut prendre un nombre quelconque de « src_labels ».",
"documentation-label-replace": "Pour chaque série chronologique dans « v », la fonction « label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string) » applique lexpression régulière « regex » à létiquette « src_label ». Si elle correspond, la série est renvoyée avec létiquette « dst_label » remplacée selon la chaîne « replacement ». « $1 » est remplacé par le premier groupe capturé, « $2 » par le second, etc. Si lexpression régulière ne correspond pas, la série est renvoyée inchangée.",
"documentation-last-over-time": "La valeur de point la plus récente dans lintervalle spécifié.",
"documentation-ln": "Calcule le logarithme népérien pour tous les éléments de « v ».\nCas particuliers :\n * « ln(+Inf) = +Inf »\n * « ln(0) = -Inf »\n * « ln(x < 0) = NaN »\n * « ln(NaN) = NaN »",
"documentation-log10": "Calcule le logarithme décimal pour tous les éléments de « v ». Les cas particuliers sont identiques à ceux de « ln ».",
"documentation-log2": "Calcule le logarithme binaire pour tous les éléments de « v ». Les cas particuliers sont identiques à ceux de « ln ».",
"documentation-max-over-time": "La valeur maximale de tous les points sur lintervalle spécifié.",
"documentation-min-over-time": "La valeur minimale de tous les points sur lintervalle spécifié.",
"documentation-minute": "Renvoie la minute de lheure pour chaque horodatage fourni (en UTC). Les valeurs renvoyées vont de 0 à 59.",
"documentation-month": "Renvoie le mois de lannée pour chaque horodatage fourni (en UTC). Les valeurs renvoyées vont de 1 à 12 (1 correspondant à janvier, etc.).",
"documentation-pi": "Renvoie la valeur de pi",
"documentation-predict-linear": "Prédit la valeur de la série chronologique à « t » secondes dans le futur, en se basant sur le vecteur dintervalle « v », à laide dune régression linéaire simple.",
"documentation-present-over-time": "La valeur 1 pour toute série dans lintervalle spécifié.",
"documentation-quantile-over-time": "Le quantile φ (0  φ  1) des valeurs dans lintervalle spécifié.",
"documentation-rad": "Convertit les degrés en radians pour tous les éléments de v",
"documentation-rate": "Calcule le taux moyen daugmentation par seconde de la série chronologique dans le vecteur dintervalle. Les ruptures de monotonie (telles que les remises à zéro du compteur dues à un redémarrage de la cible) sont automatiquement corrigées. Le calcul est également extrapolé aux extrémités de lintervalle de temps, afin de compenser déventuelles collectes manquées ou un mauvais alignement des cycles de collecte avec la période analysée.",
"documentation-resets": "Pour chaque série chronologique dentrée, « resets(v range-vector) » renvoie le nombre de remises à zéro du compteur sur la période donnée, sous forme de vecteur instantané. Toute diminution de la valeur entre deux échantillons consécutifs est interprétée comme une remise à zéro du compteur.",
"documentation-round": "Arrondit les valeurs déchantillon de tous les éléments de « v » à lentier le plus proche. En cas dégalité, larrondi est effectué vers le haut. Largument optionnel « to_nearest » permet de spécifier le multiple auquel les valeurs doivent être arrondies. Ce multiple peut être fractionnaire.",
"documentation-scalar": "À partir dun vecteur contenant un seul élément, « scalar(v instant-vector) » renvoie la valeur de cet échantillon sous forme de scalaire. Si le vecteur dentrée ne contient pas exactement un élément, « scalar » renvoie « NaN ».",
"documentation-sgn": "Renvoie un vecteur avec toutes les valeurs converties en leur signe : 1 si la valeur est positive, -1 si elle est négative et 0 si elle est nulle.",
"documentation-sort": "Renvoie les éléments du vecteur triés par valeur déchantillon, par ordre croissant.",
"documentation-sort-desc": "Renvoie les éléments du vecteur triés par valeur déchantillon, par ordre décroissant.",
"documentation-sqrt": "Calcule la racine carrée de tous les éléments de « v ».",
"documentation-stddev-over-time": "Lécart-type de la population des valeurs dans lintervalle spécifié.",
"documentation-stdvar-over-time": "La variance-type de la population des valeurs dans lintervalle spécifié.",
"documentation-sum-over-time": "La somme de toutes les valeurs dans lintervalle spécifié.",
"documentation-time": "Renvoie le nombre de secondes écoulées depuis le 1er janvier 1970 (UTC). À noter : cette valeur ne correspond pas à lheure actuelle, mais à linstant auquel lexpression est évaluée.",
"documentation-timestamp": "Renvoie lhorodatage de chaque échantillon du vecteur fourni, sous forme de nombre de secondes depuis le 1er janvier 1970 (UTC).",
"documentation-vector": "Renvoie le scalaire « s » sous forme de vecteur sans étiquette.",
"documentation-year": "Renvoie lannée correspondant à chaque horodatage fourni (en UTC)."
},
"getTrigonometricFunctions": {
"documentation-acos": "calcule larc cosinus de tous les éléments dans {{argument}}",
"documentation-acosh": "calcule le cosinus hyperbolique inverse de tous les éléments dans {{argument}}",
"documentation-asin": "calcule larc sinus de tous les éléments dans {{argument}}",
"documentation-asinh": "calcule le sinus hyperbolique inverse de tous les éléments dans {{argument}}",
"documentation-atan": "calcule larc tangente de tous les éléments dans {{argument}}",
"documentation-atanh": "calcule la tangente hyperbolique inverse de tous les éléments dans {{argument}}",
"documentation-cos": "calcule le cosinus de tous les éléments dans {{argument}}",
"documentation-cosh": "calcule le cosinus hyperbolique de tous les éléments dans {{argument}}",
"documentation-sin": "calcule le sinus de tous les éléments dans {{argument}}",
"documentation-sinh": "calcule le sinus hyperbolique de tous les éléments dans {{argument}}",
"documentation-tan": "calcule la tangente de tous les éléments dans {{argument}}",
"documentation-tanh": "calcule la tangente hyperbolique de tous les éléments dans {{argument}}"
}
},
"querybuilder": {
"feedback-link": {
"give-feedback": "Publiez votre commentaire",
"title-give-feedback": "Lexplorateur de métriques est une nouvelle fonctionnalité ; faites-nous part de vos retours pour laméliorer"
},
"get-collapsed-info": {
"exemplars": "Exemples : {{value}}",
"format": "Format : {{value}}",
"legend": "Légende : {{value}}",
"step": "Étape : {{value}}",
"type": "Type : {{value}}"
},
"get-placeholders": {
"browse": "Rechercher des métriques par nom",
"type": "Filtrer par type"
},
"get-prom-types": {
"description-counter": "Une métrique cumulative qui représente un compteur croissant dont la valeur ne peut quaugmenter ou être réinitialisée à zéro lors dun redémarrage.",
"description-gauge": "Une métrique représentant une valeur numérique unique pouvant fluctuer librement.",
"description-histogram": "Un histogramme échantillonne des observations (par exemple, durées de requêtes ou tailles de réponse) et les répartit dans des classes configurables.",
"description-native-histogram": "Les histogrammes natifs diffèrent des histogrammes classiques de Prometheus à plusieurs égards : leurs bornes de classe sont calculées à partir dune formule dépendant de leur résolution et ne sont pas définies par lutilisateur.",
"description-no-type": "Ces métriques nont pas de type défini dans les métadonnées.",
"description-summary": "Un résumé échantillonne des observations (par exemple, durées de requêtes ou tailles de réponse) et permet de calculer des quantiles configurables sur une fenêtre temporelle glissante.",
"description-unknown": "Ces métriques sont marquées comme inconnues dans les métadonnées.",
"label-counter": "Compteur",
"label-gauge": "Jauge",
"label-histogram": "Histogramme",
"label-native-histogram": "Histogramme natif",
"label-no-type": "Aucun type",
"label-summary": "Résumé",
"label-unknown": "Inconnu"
},
"handle-function": {
"text": {
"query-parsing-is-ambiguous": "Lanalyse de la requête est ambiguë."
}
},
"label-filter-item": {
"aria-label-remove": "Supprimer {{name}}",
"placeholder-select-label": "Sélectionner une étiquette",
"placeholder-select-value": "Sélectionner une valeur"
},
"label-filters": {
"label-filters": "Filtres détiquettes",
"label-label-filters": "Filtres détiquettes",
"tooltip-label-filters": "Facultatif : utilisé pour filtrer la sélection de métrique pour ce type de requête."
},
"label-param-editor": {
"loadingMessage-loading-labels": "Chargement des étiquettes",
"noOptionsMessage-no-labels-found": "Aucune étiquette trouvée"
},
"metric-combobox": {
"async-select": {
"aria-label-open-metrics-explorer": "Ouvrir lexplorateur de métriques",
"placeholder-select-metric": "Sélectionner la métrique",
"tooltip-open-metrics-explorer": "Ouvrir lexplorateur de métriques"
},
"label-metric": "Métrique",
"tooltip-metric": "Facultatif : renvoie une liste de valeurs détiquette pour le nom détiquette dans la métrique spécifiée."
},
"metrics-modal": {
"aria-label-browse-metrics": "Parcourir les métriques",
"currently-selected": "Sélection actuelle : {{selected}}",
"metrics-pre-filtered": "Ces métriques ont été pré-filtrées par les étiquettes choisies dans les filtres détiquettes.",
"title-metrics-explorer": "Explorateur de métriques"
},
"nested-query": {
"label": {
"ignoring": "Ignorer",
"on": "Activée"
},
"operator": "Opérateur",
"tooltip-remove-match": "Supprimer la correspondance",
"vector-matches": "Correspondances de vecteur"
},
"operation-editor": {
"not-found": "Opération {{id}} introuvable",
"title-remove": "Supprimer {{name}}"
},
"operation-header": {
"placeholder-replace-with": "Remplacer par",
"title-click-to-view-alternative-operations": "Cliquer pour afficher les autres opérations",
"title-remove-operation": "Supprimer lopération"
},
"operation-info-button": {
"title-click-to-show-description": "Cliquer pour afficher la description",
"title-remove-operation": "Supprimer lopération"
},
"operation-list": {
"operations": "Opérations",
"placeholder-search": "Rechercher",
"title-add-operation": "Ajouter une opération"
},
"operation-param-editor": {
"title-add": "Ajouter {{name}}",
"title-remove": "Supprimer {{name}}"
},
"operation-utils": {
"getAggregationExplainer": {
"label-by": "Calcule {{aggregationName}} sur les dimensions tout en conservant {{labelWord}} {{labels}}.",
"label-default": "Calcule {{aggregationName}} sur les dimensions.",
"label-without": "Calcule {{aggregationName}} sur les dimensions {{labels}}. Toutes les autres étiquettes sont conservées."
}
},
"prom-query-builder-options": {
"aria-label-exemplars": "Commutateur dexemples.",
"aria-label-format": "Liste déroulante de format",
"aria-label-lower-limit-parameter": "Champ de texte de pas minimal ; définit la limite inférieure du paramètre de pas",
"aria-label-select-resolution": "Sélectionner une résolution",
"aria-label-type": "Groupe de boutons radio de type",
"format-options": {
"label-heatmap": "Carte thermique",
"label-table": "Tableau",
"label-time-series": "Série chronologique"
},
"label-exemplars": "Exemples",
"label-format": "Format",
"label-min-step": "Étape minimum",
"label-resolution": "Résolution",
"label-type": "Type",
"placeholder-auto": "auto",
"title-options": "Options",
"tooltip-min-step": "Une limite inférieure supplémentaire pour le paramètre détape de la requête Prometheus et pour les variables <2>{{interval}}</2> et <4>{{rateInterval}}</4>."
},
"prom-query-editor-selector": {
"body-syntax-error": "Une erreur de syntaxe ou une structure de requête invalide empêche laffichage en mode Builder. Certaines parties de la requête peuvent être perdues.",
"confirmText-continue": "Continuer",
"kick-start-your-query": "Lancer votre requête",
"label-explain": "Expliquer",
"run-queries": "Exécuter les requêtes",
"title-parsing-error-switch-builder": "Erreur danalyse : passer en mode Créateur ?"
},
"prom-query-legend-editor": {
"aria-label-legend": "Liste déroulante de légende",
"label-legend": "Légende",
"placeholder-select-legend-mode": "Sélectionner le mode légende",
"tooltip-legend": "Remplacement ou modèle de nom de série. Par exemple, {{templateExample}} sera remplacé par la valeur de létiquette pour {{labelName}}."
},
"query-builder-hints": {
"hint-details": "indice : {{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "Builder",
"label-code": "Code"
}
},
"query-pattern": {
"apply-query": "Appliquer la requête",
"aria-label-apply-query-starter-button": "bouton appliquer le démarrage de la requête",
"aria-label-back-button": "bouton retour",
"aria-label-create-new-query-button": "bouton créer une requête",
"aria-label-raw-query": "Requête brute {{patternName}}",
"aria-label-use-this-query-button": "bouton utiliser cette requête",
"back": "Précédent",
"create-new-query": "Créer une requête",
"use-this-query": "Utiliser cette requête"
},
"query-patterns-modal": {
"aria-label-close-kick-start-your-query-modal": "fermer la fenêtre modale lancer votre requête",
"aria-label-kick-start-your-query-modal": "Lancer votre requête modale",
"aria-label-toggle-query-starter": "ouvrir et fermer la carte de démarrage de la requête {{patternType}}",
"close": "Fermer",
"description-kick-start-your-query": "Lancez votre requête en sélectionnant lune de ces requêtes. Vous pouvez ensuite continuer à compléter votre requête.",
"label-toggle-query-starter": "démarreurs de requête {{patternType}}",
"title-kick-start-your-query": "Lancer votre requête"
},
"raw-query": {
"aria-label-selector": "sélecteur"
},
"results-table": {
"content-descriptive-type": "Lors de la création dun(e) {{descriptiveType}}, Prometheus expose plusieurs séries avec le compteur de type. ",
"description": "Description",
"message-expand-label-filters": "Aucune métrique trouvée. Essayez délargir vos filtres détiquettes.",
"message-expand-search": "Aucune métrique trouvée. Essayez délargir votre recherche et vos filtres.",
"message-no-metrics-found": "Aucune métrique trouvée dans la source de données.",
"name": "Nom",
"type": "Type"
}
}
}
}

View File

@@ -1,492 +0,0 @@
{
"grafana-prometheus": {
"components": {
"annotation-query-editor": {
"annotation-data-load-error": "Hiba a jegyzetadatok betöltése során.",
"aria-label-lower-limit-parameter": "Állítsa be a léptékparaméter alsó határát",
"label-min-step": "Min. lépték",
"label-series-value-as-timestamp": "Sorozatérték időbélyegként",
"label-tags": "Címkék",
"label-text": "Szöveg",
"label-title": "Cím",
"placeholder-auto": "automatikus",
"tooltip-either-pattern-example-instance-replaced-label": "Használja a nevet vagy egy mintázatot. Például a(z) {{labelTemplate}} helyébe a(z) {{labelName}} címke címkeértéke lép.",
"tooltip-min-step": "További alsó határérték a Prometheus-lekérdezés lépésparaméteréhez, valamint a(z) <2>{{intervalVar}}</2> és <4>{{rateIntervalVar}}</4> változóhoz.",
"tooltip-timestamp-milliseconds-series-value-seconds-multiply": "Az időbélyeg mértékegysége az ezredmásodperc. Ha a sorozatérték mértékegysége a másodperc, szorozza meg a tartományvektorát 1000-rel."
},
"get-query-type-options": {
"description": {
"instant-query-range": "Azonnali lekérdezés és tartománylekérdezés futtatása"
},
"label": {
"both": "Mindkettő"
},
"range-options": {
"description": {
"query-range": "Lekérdezés futtatása egy időtartományban"
},
"label": {
"instant": "Azonnali",
"range": "Tartomány"
}
}
},
"label-selector": {
"aria-label-filter-expression-for-label": "Szűrőkifejezés címkéhez",
"description-select-labels": "A címkeértékek kiválasztása után csak a lehetséges címkekombinációk jelennek meg.",
"select-labels-to-search-in": "2. Válassza ki a kereséshez használni kívánt címkéket"
},
"metric-selector": {
"aria-label-filter-expression-for-metric": "Szűrőkifejezés a metrikához",
"aria-label-limit-results-from-series-endpoint": "A sorozat végpontjáról származó eredmények korlátozása",
"description-series-limit": "A korlát minden metrikára, címkére és értékre vonatkozik. Hagyja üresen a mezőt az alapértelmezett korlát használatához. Állítsa 0-ra a korlát letiltásához és az összes lekéréséhez ez teljesítményproblémákat okozhat.",
"label-select-metric": "A metrika kiválasztása után csak a lehetséges címkék jelennek meg. A címkéket az alábbi sorozatkorlát korlátozza.",
"select-a-metric": "1. Válasszon egy metrikát",
"series-limit": "Sorozatkorlát"
},
"prom-cheat-sheet": {
"prom-ql-cheat-sheet": "PromQL-puska"
},
"prom-exemplar-field": {
"exemplars": "Mintapéldányok",
"tooltip-disable-query": "Mintapéldányokkal rendelkező lekérdezések letiltása",
"tooltip-enable-query": "Mintapéldányokkal rendelkező lekérdezések engedélyezése"
},
"prom-explore-extra-field": {
"aria-label-prometheus-extra-field": "Prometheus extra mező",
"aria-label-query-type-field": "Lekérdezéstípus mező",
"aria-label-step-field": "Lépték mező",
"min-step": "Min. lépték",
"query-type": "Lekérdezés típusa",
"tooltip-units-builtin-variables-example-interval-rateinterval": "Itt időegységek és beépített változók használhatók, például: {{example1}}, {{example2}}, {{example3}}, {{example4}}, {{example5}}, {{example6}}, {{example7}} (alapértelmezett, ha nincs megadva mértékegység: {{default}})"
},
"prom-query-field": {
"placeholder-enter-a-prom-ql-query": "Írjon be egy PromQL-lekérdezést…"
},
"prom-variable-query-editor": {
"aria-label-classic-query": "Klasszikus lekérdezés",
"aria-label-metric-regex": "Metrika reguláris kifejezése",
"aria-label-metric-selector": "Metrikaválasztó",
"aria-label-prometheus-query": "Prometheus-lekérdezés",
"aria-label-query-type": "Lekérdezés típusa",
"aria-label-series-query": "Sorozatlekérdezés",
"label-classic-query": "Klasszikus lekérdezés",
"label-label": "Címke",
"label-metric-regex": "Metrika reguláris kifejezése",
"label-query": "Lekérdezés",
"label-query-type": "Lekérdezés típusa",
"label-series-query": "Sorozatlekérdezés",
"placeholder-classic-query": "Klasszikus lekérdezés",
"placeholder-metric-regex": "Metrika reguláris kifejezése",
"placeholder-prometheus-query": "Prometheus-lekérdezés",
"placeholder-select-query-type": "Lekérdezéstípus kiválasztása",
"placeholder-series-query": "Sorozatlekérdezés",
"returns-metrics-matching-specified-metric-regex": "A megadott metrikai reguláris kifejezésnek megfelelő metrikák listáját adja vissza.",
"tooltip-classic-query": "A Prometheus változólekérdezés-szerkesztő eredeti implementációja. Adjon meg egy karakterláncot a megfelelő lekérdezéstípussal és paraméterekkel, a jelen dokumentumokban leírtak szerint. Például: {{exampleQuery}}.",
"tooltip-label": "A címke nevéhez tartozó címkeértékek listáját adja vissza az összes metrikában, kivéve, ha a metrika meg van adva.",
"tooltip-metric-regex": "A címkenevek listáját adja vissza, opcionálisan a megadott metrikai reguláris kifejezés szerint szűrve.",
"tooltip-query": "A lekérdezéshez a Prometheus-lekérdezési eredmények listáját adja vissza. Ez magában foglalhatja a Prometheus-funkciókat, azaz {{exampleQuery}}.",
"tooltip-query-type": "A Prometheus adatforrás-beépülőmodul a következő lekérdezéstípusokat biztosítja a sablonváltozókhoz.",
"tooltip-series-query": "Adjon meg egy metrikát címkékkel, csak egy metrikát vagy csak címkéket: {{example1}}, {{example2}} vagy {{example3}}. A bevitt adatokhoz társított idősorok listáját adja vissza."
},
"selector-actions": {
"aria-label-selector": "választó",
"aria-label-selector-clear-button": "Választó törlése gomb",
"aria-label-use-selector-as-metrics-button": "Választó használata metrikaként gomb",
"aria-label-use-selector-for-query-button": "Választó használata lekérdezéshez gomb",
"aria-label-validate-submit-button": "Küldés érvényesítése gomb",
"clear": "Törlés",
"resulting-selector": "4. Eredményként kapott választó",
"use-as-rate-query": "Használat értékeléses lekérdezésként",
"use-query": "Lekérdezés használata",
"validate-selector": "Választó érvényesítése"
},
"value-selector": {
"aria-label-filter-expression-for-label-values": "Szűrőkifejezés címkeértékekhez",
"aria-label-values-for": "Értékek a következőhöz: {{labelKey}}",
"description-search-field-values-across-selected-labels": "A keresőmező segítségével kereshet értékeket a kiválasztott címkék között.",
"select-multiple-values-for-your-labels": "3. Válasszon (több) értéket a címkéihez"
}
},
"configuration": {
"alerting-settings-overhaul": {
"label-allow-as-recording-rules-target": "Engedélyezés felvételkészítési szabályok céljaként",
"label-manage-alerts-via-alerting-ui": "Riasztások kezelése az Alerting kezelőfelületén keresztül",
"title-alerting": "Riasztás",
"tooltip-allow-as-recording-rules-target": "Engedélyezze, hogy ez az adatforrás célként legyen kiválasztva a felvételkészítési szabályok írásához.",
"tooltip-manage-alerts-via-alerting-ui": "Riasztási szabályok kezelése ehhez az adatforráshoz. Más Alerting-erőforrások kezeléséhez adjon hozzá egy Alertmanager-adatforrást."
},
"config-editor": {
"browser-access-mode-error": "A Prometheus-adatforrás böngészős hozzáférési módja már nem érhető el. Váltás kiszolgáló-hozzáférési módra.",
"description-advanced-settings": "A további beállítások olyan opcionális beállítások, amelyek konfigurálhatók az adatforrás nagyobb mértékű ellenőrzése érdekében.",
"title-advanced-settings": "Speciális beállítások",
"title-error": "Hiba"
},
"data-source-http-settings-overhaul": {
"tooltip-browser-access-mode": "Az Ön hozzáférési módja <1>Böngésző</1>, ez azt jelenti, hogy az URL-címnek elérhetőnek kell lennie a böngészőből.",
"tooltip-http-url": "Adjon meg egy teljes HTTP URL-címet (például: {{exampleURL}})",
"tooltip-server-access-mode": "Az Ön hozzáférési módja <1>Kiszolgáló</1>, ez azt jelenti, hogy az URL-címnek elérhetőnek kell lennie a Grafana-háttérrendszerből/-kiszolgálóról."
},
"docs-tip": {
"visit-docs-for-more-details-here": "További részletekért tekintse meg a dokumentumokat itt."
},
"exemplar-setting": {
"label-data-source": "Adatforrás",
"label-internal-link": "Belső hivatkozás",
"label-label-name": "Címkenév",
"label-remove-exemplar-link": "Mintapéldány-hivatkozás eltávolítása",
"label-url": "URL-cím",
"label-url-label": "URL-címke",
"placeholder-go-to-examplecom": "Tovább az example.com webhelyre",
"placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}",
"placeholder-trace-id": "traceID",
"title-remove-exemplar-link": "Mintapéldány-hivatkozás eltávolítása",
"tooltip-data-source": "A mintapéldány ehhez az adatforráshoz fog navigálni.",
"tooltip-internal-link": "Engedélyezze ezt a lehetőséget, ha van belső hivatkozása. Ha engedélyezve van, megjeleníti az adatforrás-választót. Válassza ki a háttérrendszeri követési adattárat a mintapéldány-adatokhoz.",
"tooltip-label-name": "A címkeobjektum azon mezőjének neve, amelyet a traceID lekéréséhez kell használni.",
"tooltip-url": "A nyomkövetési háttérrendszer URL-címe, ahová a felhasználó menne, hogy lássa a nyomkövetést",
"tooltip-url-label": "Használja a gombcímke felülbírálásához a mintapéldány traceID mezőjén."
},
"exemplars-settings": {
"add": "Hozzáadás",
"no-exemplars-configurations": "Nincsenek mintapéldány-konfigurációk",
"title-exemplars": "Mintapéldányok"
},
"prom-settings": {
"aria-label-default-editor": "Alapértelmezett szerkesztő (kód vagy építő)",
"aria-label-prom-type-type": "{{promType}} típusa",
"aria-label-prometheus-type": "Prometheus-típus",
"aria-label-select-http-method": "HTTP-módszer kiválasztása",
"editor-options": {
"label-builder": "Építő",
"label-code": "Kód"
},
"label-cache-level": "Gyorsítótár szintje",
"label-custom-query-parameters": "Egyéni lekérdezési paraméterek",
"label-default-editor": "Alapértelmezett szerkesztő",
"label-disable-metrics-lookup": "Metrikák keresésének letiltása",
"label-disable-recording-rules-beta": "Felvételkészítési szabályok letiltása (béta)",
"label-http-method": "HTTP-metódus",
"label-incremental-querying-beta": "Növekményes lekérdezés (béta)",
"label-prom-type-version": "{{promType}} verziója",
"label-prometheus-type": "Prometheus-típus",
"label-query-overlap-window": "Lekérdezésátfedési ablak",
"label-query-timeout": "Lekérdezési időtúllépés",
"label-scrape-interval": "Adatgyűjtési intervallum",
"label-series-limit": "Sorozatkorlát",
"label-use-series-endpoint": "Sorozat végpontjának használata",
"more-info": "A Prometheus-típus és -verzió adatforrásokban történő konfigurálásával kapcsolatos további információkért tekintse meg a <2>kiépítési dokumentációt</2>.",
"placeholder-example-maxsourceresolutionmtimeout": "Példa: {{example}}",
"title-interval-behaviour": "Intervallum viselkedése",
"title-other": "Egyéb",
"title-performance": "Teljesítmény",
"title-query-editor": "Lekérdezésszerkesztő",
"tooltip-cache-level": "Beállítja a böngésző gyorsítótárának szintjét a szerkesztői lekérdezésekhez. A magasabb számossági adatforrásokhoz magasabb gyorsítótár-beállítások ajánlottak.",
"tooltip-custom-query-parameters": "Egyéni paraméterek hozzáadása a Prometheus lekérdezési URL-jéhez. Például: {{example1}}, {{example2}}, {{example3}} vagy {{example4}}. Több paramétert a következővel kell összefűzni: {{concatenationChar}}.",
"tooltip-default-editor": "Beállítja az alapértelmezett szerkesztőopciót az adatforrás minden felhasználója számára.",
"tooltip-disable-metrics-lookup": "Ennek az opciónak a bejelölése letiltja a metrikaválasztót és a metrika-/címketámogatást a lekérdezési mező automatikus kitöltésében. Ez segít, ha a nagyobb Prometheus-példányokkal teljesítményproblémái vannak. ",
"tooltip-disable-recording-rules-beta": "Ez a funkció letiltja a felvételkészítési szabályokat. Kapcsolja be az irányítópult teljesítményének javítása érdekében",
"tooltip-http-method": "A Prometheus-adatforrás lekérdezéséhez használhatja a POST- vagy a GET HTTP-metódust. A POST az ajánlott metódus, mivel nagyobb lekérdezéseket tesz lehetővé. Módosítsa ezt GET-re, ha a Prometheus-verziója régebbi, mint a 2.1-es, vagy ha a POST-kérések korlátozva vannak a hálózatában.",
"tooltip-incremental-querying-beta": "Ez a funkció megváltoztatja a relatív lekérdezések azon alapértelmezett viselkedését, hogy mindig friss adatokat kérjenek le a Prometheus-példányból, ehelyett a lekérdezési eredményeket a rendszer gyorsítótárazza, és csak új rekordok lesznek lekérve. Kapcsolja be az adatbázis és a hálózat terhelésének csökkentése érdekében.",
"tooltip-prom-type-version": "Ezzel állíthatja be a(z) {{promType}}-példány verzióját, ha nincs automatikusan konfigurálva.",
"tooltip-prometheus-type": "Állítsa be a Prometheus-adatbázis típusára, pl.: Prometheus, Cortex, Mimir vagy Thanos. A mező módosításával menti az aktuális beállításokat. A Prometheus bizonyos típusai támogatják vagy nem támogatják a különböző API-kat. Például egyes típusok támogatják a reguláris kifejezéses egyeztetést a címkelekérdezésekhez a teljesítmény javítása érdekében. Egyes típusok rendelkeznek API-val a metaadatokhoz. Ha ezt helytelenül állítja be, furcsa viselkedést tapasztalhat a metrikák és címkék lekérdezésekor. Ellenőrizze a Prometheus dokumentációját, hogy biztosan a megfelelő típust adja meg.",
"tooltip-query-overlap-window": "Állítson be egy időtartamot, például: {{example1}}, vagy {{example2}}, vagy {{example3}}. A(z) {{default}} alapértelmezése. Ez az időtartam hozzáadódik az egyes növekményes kérések időtartamához.",
"tooltip-query-timeout": "Állítsa be a Prometheus lekérdezési időtúllépését.",
"tooltip-scrape-interval": "Ez az intervallum azt mutatja, hogy a Prometheus milyen gyakran gyűjt adatokat a célokról. Állítsa be a Prometheus konfigurációs fájljában konfigurált tipikus adatgyűjtési és értékelési intervallumra. Ha ezt nagyobb értékre állítja, mint a Prometheus konfigurációs fájljának intervalluma, a Grafana ennek az intervallumnak megfelelően értékeli az adatokat, és kevesebb adatpontot fog látni. Alapértelmezett érték: {{default}}.",
"tooltip-series-limit": "A korlát minden erőforrásra (metrikára, címkére és értékre) vonatkozik mindkét végpont (sorozatok és címkék) esetében. Hagyja üresen a mezőt az alapértelmezett korlát (40 000) használatához. Állítsa 0-ra a korlát letiltásához és az összes lekéréséhez ez teljesítményproblémákat okozhat. Az alapértelmezett korlát 40 000.",
"tooltip-use-series-endpoint": "Ennek az opciónak a bejelölésével előnyben részesíti a sorozat végpontját {{exampleParameter}} paraméterrel a címkeértékek végpontjához képest {{exampleParameter}} paraméterrel. Bár a címkeértékek végpontja hatékonyabbnak tekinthető, egyes felhasználók előnyben részesíthetik a sorozatot, mert rendelkezik POST-metódussal, míg a címkeértékek végpontja csak GET-metódussal rendelkezik."
}
},
"metrics-browser": {
"disabled-label": "(Letiltva)",
"enabled-label": "Metrikaböngésző"
},
"prom-query-legend-editor": {
"get-legend-mode-options": {
"description-auto": "Csak egyedi címkéket tartalmaz",
"description-custom": "Adjon meg egy elnevezési sablont",
"description-verbose": "Minden címkenév és -érték",
"label-auto": "Automatikus",
"label-custom": "Egyéni",
"label-verbose": "Részletes"
}
},
"promql": {
"getAggregationOptions": {
"documentation-avg": "Számítsa ki a méretek átlagát",
"documentation-bottomk": "A legkisebb k elem mintaérték szerint",
"documentation-count": "Számolja meg a vektor elemeinek számát",
"documentation-count-values": "Számolja meg az azonos értékű elemek számát",
"documentation-group": "Az eredményül kapott vektor összes értéke: 1",
"documentation-max": "Válassza ki a maximális méreteket",
"documentation-min": "Válassza ki a minimális méreteket",
"documentation-quantile": "Számítsa ki a φ-kvantilist (0 ≤ φ ≤ 1) a dimenziók mentén",
"documentation-stddev": "Számítsa ki a populáció szórását a dimenziók mentén",
"documentation-stdvar": "Számítsa ki a populáció szabványos varianciáját a dimenziók mentén",
"documentation-sum": "Számítsa ki az összeget a dimenziók mentén",
"documentation-topk": "A legnagyobb k elemek mintaérték szerint"
},
"getFunctions": {
"documentation-abs": "Visszaadja a bemeneti vektort, amelyben az összes mintaérték abszolút értékre van konvertálva.",
"documentation-absent": "Üres vektort ad vissza, ha az átadott vektor tartalmaz elemeket, és 1 elemből álló vektort ad vissza, amelynek értéke 1, ha az átadott vektor nem tartalmaz elemeket. Ez akkor hasznos, ha figyelmeztetésre van szükség, amikor egy adott metrika név-címke kombinációjához nem létezik idősor.",
"documentation-absent-over-time": "Üres vektort ad vissza, ha az átadott tartományvektor tartalmaz elemeket, és 1 elemből álló vektort ad vissza, amelynek értéke 1, ha az átadott tartományvektor nem tartalmaz elemeket.",
"documentation-avg-over-time": "A megadott intervallum összes pontjának átlagértéke.",
"documentation-ceil": "A „v” összes elemének mintavételi értékeit a legközelebbi egész számra felkerekíti.",
"documentation-changes": "Minden bemeneti idősor esetében a „changes(v range-vector)” függvény az adott időtartományon belül az érték változásának számát adja vissza pillanatnyi vektorként.",
"documentation-clamp": "A „v” összes elemének mintavételi értékeit úgy rögzíti, hogy az alsó határ „min”, a felső határ pedig „max” legyen.",
"documentation-clamp-max": "A „v” összes elemének mintavételi értékeit úgy rögzíti, hogy a felső határ „max” legyen.",
"documentation-clamp-min": "A „v” összes elemének mintavételi értékeit úgy rögzíti, hogy az alsó határ „min” legyen.",
"documentation-count-over-time": "A megadott intervallum összes értékének száma.",
"documentation-count-scalar": "Visszaadja az idősorvektor elemeinek számát skalárként. Ez ellentétben áll a „count()” összesítő operátorral, amely mindig vektort ad vissza (üreset, ha a bemeneti vektor üres) és lehetővé teszi a címkék szerinti csoportosítást a „by” záradékkal.",
"documentation-day-of-month": "Visszaadja az időpontok adott hónapra vonatkozó napját UTC-ben. A visszaadott értékek 1 és 31 között lehetnek.",
"documentation-day-of-week": "Visszaadja az időpontok adott hétre vonatkozó napját UTC-ben. A visszaadott értékek 0 és 6 között lehetnek, ahol a 0 vasárnapot jelent stb.",
"documentation-day-of-year": "Visszaadja az időpontok adott évre vonatkozó napját UTC-ben. A visszaadott értékek 1 és 365 között lehetnek a nem szökőévek esetében, és 1 és 366 között a szökőévek esetében.",
"documentation-days-in-month": "Visszaadja az adott időpontokhoz tartozó hónap napjainak számát UTC időzóna szerint. A visszaadott értékek 28 és 31 között lehetnek.",
"documentation-deg": "A v összes elemének radiánjait fokokká alakítja át",
"documentation-delta": "Kiszámítja a „v” tartományvektorban szereplő idősorelemek első és utolsó értéke közötti különbséget, és visszaad egy pillanatnyi vektort a megadott deltákkal és egyenértékű címkékkel. A delta extrapolálásra kerül, hogy lefedje a tartományvektor-választóban megadott teljes időtartományt, így lehetséges, hogy nem egész számú eredményt kapunk, még akkor is, ha a mintaértékek mind egész számok.",
"documentation-deriv": "Egyszerű lineáris regresszió segítségével kiszámítja az idősor másodpercenkénti deriváltját egy „v” tartományvektorban.",
"documentation-double-exponential-smoothing": "A „v” tartomány alapján simított értéket állít elő az idősorokhoz. Minél alacsonyabb az „sf” simítási tényező, annál nagyobb jelentőséget kapnak a régebbi adatok. Minél magasabb a „tf” trendtényező, annál nagyobb szerepet kapnak az adatokban a trendek. Mind az „sf”, mind a „tf” értékének 0 és 1 között kell lennie.",
"documentation-drop-common-labels": "Eltávolít minden olyan címkét, amely azonos névvel és értékkel rendelkezik a bemeneti vektor összes sorozatában.",
"documentation-exp": "Kiszámítja az exponenciális függvényt a „v” összes elemére.\nKülönleges esetek:\n* „Exp(+Inf) = +Inf”\n* „Exp(NaN) = NaN”",
"documentation-floor": "A „v” összes elemének mintavételi értékeit a legközelebbi egész számra lekerekíti.",
"documentation-histogram-avg": "Visszaadja a natív hisztogramban tárolt megfigyelt értékek számtani átlagát. A natív hisztogramokhoz nem tartozó minták figyelmen kívül maradnak, és nem jelennek meg a visszaadott vektorban.",
"documentation-histogram-count": "Visszaadja a natív hisztogramban tárolt megfigyelések számát.",
"documentation-histogram-fraction": "Visszaadja a megadott alsó és felső értékek közötti megfigyelések becsült arányát.",
"documentation-histogram-quantile": "Kiszámítja a φ-kvantilt (0 ≤ φ ≤ 1) a hisztogram „b” gyűjtőiből. A „b” minták az egyes gyűjtőkben található megfigyelések számát jelentik. Minden mintának rendelkeznie kell egy „le” címkével, amelynek értéke a gyűjtő felső határát jelöli. (Az ilyen címkével nem rendelkező mintákat a rendszer figyelmen kívül hagyja.) A hisztogram metrikus típusa automatikusan ellátja az idősorokat a „_bucket” utótaggal és a megfelelő címkékkel.",
"documentation-histogram-stddev": "Visszaadja a natív hisztogramban szereplő megfigyelések becsült szórását a megfigyelések helyét tartalmazó tartományok geometriai átlaga alapján.",
"documentation-histogram-stdvar": "Visszaadja a natív hisztogramban szereplő megfigyelések becsült varianciáját.",
"documentation-histogram-sum": "Visszaadja a natív hisztogramban tárolt megfigyelések összegét.",
"documentation-holt-winters": "A Prometheus v3.x verzióban double_exponential_smoothing megnevezést kapott. A Prometheus v3.0 vagy annál újabb verzióknál használja a double_exponential_smoothing funkciót.\n\nA „v” tartomány alapján simított értéket állít elő az idősorokhoz. Minél alacsonyabb az „sf” simítási tényező, annál nagyobb jelentőséget kapnak a régebbi adatok. Minél magasabb a „tf” trendtényező, annál nagyobb szerepet kapnak az adatokban a trendek. Mind az „sf”, mind a „tf” értékének 0 és 1 között kell lennie.",
"documentation-hour": "Visszaadja az adott időpontok óráját UTC-ben. A visszaadott értékek 0 és 23 között lehetnek.",
"documentation-idelta": "Kiszámítja a „v” tartományvektor utolsó két mintája közötti különbséget, és visszaad egy pillanatnyi vektort a megadott deltákkal és egyenértékű címkékkel.",
"documentation-increase": "Kiszámítja az idősor növekedését a tartományvektorban. A monotonitás megszakadásait (például a cél újraindítása miatt bekövetkező számláló-visszaállításokat) a rendszer automatikusan kiigazítja. A növekedés extrapolálásra kerül, hogy lefedje a tartományvektor-választóban megadott teljes időtartományt, így lehetséges, hogy nem egész számú eredményt kapunk, még akkor is, ha a számláló csak egész számmal növekszik.",
"documentation-info": "A legfrissebb részleteket és metaadatokat adja vissza egy mutatócsoportról, például a címkéket és az aktuális értékeket, anélkül, hogy bármilyen számítást végezne.",
"documentation-irate": "Kiszámítja az idősor másodpercenkénti növekedési sebességét a tartományvektorban. Ez az utolsó két adatponton alapul. A monotonitás megszakadásait (például a cél újraindítása miatt bekövetkező számláló-visszaállításokat) a rendszer automatikusan kiigazítja.",
"documentation-label-join": "A „v” vektor minden idősorához összekapcsolja az összes „src_labels” értékét a „separator” segítségével, és visszaadja az összekapcsolt értéket tartalmazó, „dst_label” címkével ellátott idősorokat. Ebben a függvényben tetszőleges számú „src_labels” lehet.",
"documentation-label-replace": "A „v” minden idősorához a „label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)” a „regex” reguláris kifejezést a „src_label” címkével hasonlítja össze. Ha egyezik, akkor az idősor a „dst_label” címkét adja vissza, amelyet a „replacement” kiterjesztése helyettesít. Az „$1” helyére az első egyező alcsoport, az „$2” helyére a második stb. kerül. Ha a reguláris kifejezés nem egyezik, akkor az idősor változatlanul kerül visszaadásra.",
"documentation-last-over-time": "A legutóbbi pontérték a megadott intervallumban.",
"documentation-ln": "Kiszámítja a természetes logaritmust a „v” összes elemére.\nKülönleges esetek:\n * „ln(+Inf) = +Inf”\n * „ln(0) = -Inf”\n * „ln(x < 0) = NaN”\n * „ln(NaN) = NaN”",
"documentation-log10": "Kiszámítja a „v” összes elemének decimális logaritmusát. A különleges esetek megegyeznek az „ln” esetével.",
"documentation-log2": "Kiszámítja a „v” összes elemének bináris logaritmusát. A különleges esetek megegyeznek az „ln” esetével.",
"documentation-max-over-time": "A megadott intervallum összes pontjának maximális értéke.",
"documentation-min-over-time": "A megadott intervallum összes pontjának minimális értéke.",
"documentation-minute": "Visszaadja az adott időpontok percét UTC-ben. A visszaadott értékek 0 és 59 között lehetnek.",
"documentation-month": "Visszaadja az év hónapját az adott időpontokhoz UTC-ben. A visszaadott értékek 1 és 12 között lehetnek, ahol 1 január, stb.",
"documentation-pi": "A pi értékét adja vissza",
"documentation-predict-linear": "Egyszerű lineáris regresszióval megjósolja a „t” másodperccel későbbi idősor értékét a „v” tartományvektor alapján.",
"documentation-present-over-time": "Az 1-es érték a megadott intervallum bármely sorozata esetében.",
"documentation-quantile-over-time": "A megadott intervallum értékeinek φ-kvantilise (0 ≤ φ ≤ 1).",
"documentation-rad": "A „v” összes elemének fokjait radiánra alakítja át",
"documentation-rate": "Kiszámítja az idősor másodpercenkénti átlagos növekedési sebességét a tartományvektorban. A monotonitás megszakadásait (például a cél újraindítása miatt bekövetkező számláló-visszaállításokat) a rendszer automatikusan kiigazítja. Ezenkívül a számítás extrapolálja az időtartomány végeit, figyelembe véve a kihagyott lekérdezéseket vagy a lekérdezési ciklusok és a tartomány időtartamának nem tökéletes összehangolását.",
"documentation-resets": "Minden bemeneti idősor esetében a „resets(v range-vector)” a megadott időtartományon belüli számláló-visszaállítások számát azonnali vektorként adja vissza. Két egymást követő minta közötti értékcsökkenést a program számláló-visszaállításként értelmez.",
"documentation-round": "A „v” összes elemének mintaértékeit a legközelebbi egész számra kerekíti. Az egyenlő értékeket felfelé kerekíti. Az opcionális „to_nearest” argumentum lehetővé teszi a mintaértékek kerekítésének legközelebbi többszörösének megadását. Ez a többszörös lehet törtszám is.",
"documentation-scalar": "Egy egyelemes bemeneti vektor esetén a „scalar(v instant-vector)” az adott egy elem mintavételi értékét skalárként adja vissza. Ha a bemeneti vektor nem pontosan egy elemmel rendelkezik, a „scalar” a „NaN” értéket adja vissza.",
"documentation-sgn": "Egy vektort ad vissza, amelyben az összes mintaérték az előjelére van konvertálva, az alábbiak szerint: 1, ha „v” pozitív, -1, ha „v” negatív, és 0, ha „v” nulla.",
"documentation-sort": "A mintavételi értékeik szerint növekvő sorrendbe rendezve adja vissza a vektorelemeket.",
"documentation-sort-desc": "A mintavételi értékeik szerint csökkenő sorrendbe rendezve adja vissza a vektorelemeket.",
"documentation-sqrt": "Kiszámítja a „v” összes elemének négyzetgyökét.",
"documentation-stddev-over-time": "A megadott intervallum értékeinek populációs szórása.",
"documentation-stdvar-over-time": "A megadott intervallum értékeinek populációs varianciája.",
"documentation-sum-over-time": "A megadott intervallum összes értékének összege.",
"documentation-time": "Visszaadja az 1970. január 1-től eltelt másodpercek számát UTC-időzóna szerint. Ne feledje, hogy ez valójában nem a jelenlegi időt adja vissza, hanem azt az időpontot, amikor a kifejezés kiértékelésre kerül.",
"documentation-timestamp": "Visszaadja az adott vektor minden mintájának időbélyegét az 1970. január 1. óta eltelt másodpercek számaként UTC-időzónában.",
"documentation-vector": "Visszaadja a skaláris „s” értéket címkék nélküli vektorként.",
"documentation-year": "Visszaadja az adott időpontok évét UTC-ben."
},
"getTrigonometricFunctions": {
"documentation-acos": "kiszámítja a(z) {{argument}} összes elemének arkusz koszinuszát",
"documentation-acosh": "kiszámítja a(z) {{argument}} összes elemének inverz hiperbolikus koszinuszát",
"documentation-asin": "kiszámítja a(z) {{argument}} összes elemének arkusz szinuszát",
"documentation-asinh": "kiszámítja a(z) {{argument}} összes elemének inverz hiperbolikus szinuszát",
"documentation-atan": "kiszámítja a(z) {{argument}} összes elemének arkusz tangensét",
"documentation-atanh": "kiszámítja a(z) {{argument}} összes elemének inverz hiperbolikus tangensét",
"documentation-cos": "kiszámítja a(z) {{argument}} összes elemének koszinuszát",
"documentation-cosh": "kiszámítja a(z) {{argument}} összes elemének hiperbolikus koszinuszát",
"documentation-sin": "kiszámítja a(z) {{argument}} összes elemének szinuszát",
"documentation-sinh": "kiszámítja a(z) {{argument}} összes elemének hiperbolikus szinuszát",
"documentation-tan": "kiszámítja a(z) {{argument}} összes elemének tangensét",
"documentation-tanh": "kiszámítja a(z) {{argument}} összes elemének hiperbolikus tangensét"
}
},
"querybuilder": {
"feedback-link": {
"give-feedback": "Visszajelzés küldése",
"title-give-feedback": "A metrikaböngésző új, és számítunk az építő jellegű visszajelzésére"
},
"get-collapsed-info": {
"exemplars": "Mintapéldányok: {{value}}",
"format": "Formátum: {{value}}",
"legend": "Jelmagyarázat: {{value}}",
"step": "Lépés: {{value}}",
"type": "Típus: {{value}}"
},
"get-placeholders": {
"browse": "Metrikák keresése név alapján",
"type": "Szűrés típus szerint"
},
"get-prom-types": {
"description-counter": "Egyetlen, monoton növekvő számlálóhoz tartozó kumulatív mutató, amelynek értéke csak növekedhet, vagy újraindításkor nullára állítható vissza.",
"description-gauge": "Egyetlen numerikus értéket képviselő mutató, amely szabadon növekedhet és csökkenhet.",
"description-histogram": "A hisztogram mintákat vesz a megfigyelésekből (általában olyanokból, mint a kérések időtartama vagy a válaszok mérete), és konfigurálható kategóriákba sorolja őket.",
"description-native-histogram": "A natív hisztogramok számos szempontból eltérnek a klasszikus Prometheus hisztogramoktól: A natív hisztogramok tartományhatárai egy képlet alapján kerülnek kiszámolásra, amely a natív hisztogram méretarányától (felbontásától) függ, és nem a felhasználó határozza meg.",
"description-no-type": "Ezeknek a mutatóknak nincs meghatározott típusa a metaadatokban.",
"description-summary": "Összefoglalja a mintavételi megfigyeléseket (általában olyanokat, mint a kérések időtartama és a válaszok mérete), és konfigurálható kvantiliseket számíthat ki egy változó időablakban.",
"description-unknown": "Ezek a mutatók a metaadatokban ismeretlen típusúak.",
"label-counter": "Számláló",
"label-gauge": "Mérőeszköz",
"label-histogram": "Hisztogram",
"label-native-histogram": "Natív hisztogram",
"label-no-type": "Nincs típus",
"label-summary": "Összegzés",
"label-unknown": "Ismeretlen"
},
"handle-function": {
"text": {
"query-parsing-is-ambiguous": "A lekérdezés elemzése nem egyértelmű."
}
},
"label-filter-item": {
"aria-label-remove": "{{name}} eltávolítása",
"placeholder-select-label": "Címke kiválasztása",
"placeholder-select-value": "Érték kijelölése"
},
"label-filters": {
"label-filters": "Címkeszűrők",
"label-label-filters": "Címkeszűrők",
"tooltip-label-filters": "Opcionális: a lekérdezéstípushoz kiválasztott metrika szűrésére szolgál."
},
"label-param-editor": {
"loadingMessage-loading-labels": "Címkék betöltése",
"noOptionsMessage-no-labels-found": "Nem található címke"
},
"metric-combobox": {
"async-select": {
"aria-label-open-metrics-explorer": "Metrikaböngésző megnyitása",
"placeholder-select-metric": "Metrika kiválasztása",
"tooltip-open-metrics-explorer": "Metrikaböngésző megnyitása"
},
"label-metric": "Metrika",
"tooltip-metric": "Opcionális: a megadott metrikában a címke nevéhez tartozó címkeértékek listáját adja vissza."
},
"metrics-modal": {
"aria-label-browse-metrics": "Metrikák böngészése",
"currently-selected": "Jelenleg kiválasztott: {{selected}}",
"metrics-pre-filtered": "Ezek a metrikák előszűrtek a címkeszűrőkben kiválasztott címkék alapján.",
"title-metrics-explorer": "Metrikaböngésző"
},
"nested-query": {
"label": {
"ignoring": "Mellőzés",
"on": "Be"
},
"operator": "Műveleti jel",
"tooltip-remove-match": "Egyezés eltávolítása",
"vector-matches": "Vektoregyezések"
},
"operation-editor": {
"not-found": "A(z) {{id}} művelet nem található",
"title-remove": "{{name}} eltávolítása"
},
"operation-header": {
"placeholder-replace-with": "Csere ezzel:",
"title-click-to-view-alternative-operations": "Kattintson az alternatív műveletek megtekintéséhez",
"title-remove-operation": "Művelet eltávolítása"
},
"operation-info-button": {
"title-click-to-show-description": "Kattintson a leírás megjelenítéséhez",
"title-remove-operation": "Művelet eltávolítása"
},
"operation-list": {
"operations": "Műveletek",
"placeholder-search": "Keresés",
"title-add-operation": "Művelet hozzáadása"
},
"operation-param-editor": {
"title-add": "{{name}} hozzáadása",
"title-remove": "{{name}} eltávolítása"
},
"operation-utils": {
"getAggregationExplainer": {
"label-by": "Kiszámítja a(z) {{aggregationName}} összesítést a dimenziók mentén, miközben megőrzi a(z) {{labelWord}} {{labels}} címkéket.",
"label-default": "Kiszámítja a(z) {{aggregationName}} összesítést a dimenziók mentén.",
"label-without": "Kiszámítja a(z) {{aggregationName}} összesítést a(z) {{labels}} dimenziók mentén. Minden más címke megmarad."
}
},
"prom-query-builder-options": {
"aria-label-exemplars": "Mintapéldányok váltása.",
"aria-label-format": "Formátum kombinált listája",
"aria-label-lower-limit-parameter": "Minimális léptetés szövegmező, beállíthatja a léptetésparaméter alsó határát",
"aria-label-select-resolution": "Felbontás kiválasztása",
"aria-label-type": "Típus választógomb-csoport",
"format-options": {
"label-heatmap": "Hőtérkép",
"label-table": "Táblázat",
"label-time-series": "Idősor"
},
"label-exemplars": "Mintapéldányok",
"label-format": "Formátum",
"label-min-step": "Min. lépték",
"label-resolution": "Felbontás",
"label-type": "Típus",
"placeholder-auto": "automatikus",
"title-options": "Beállítások",
"tooltip-min-step": "További alsó határérték a Prometheus-lekérdezés léptékparaméteréhez, valamint a(z) <2>{{interval}}</2> és <4>{{rateInterval}}</4> változóhoz."
},
"prom-query-editor-selector": {
"body-syntax-error": "Szintaktikai hiba történt, vagy a lekérdezési struktúra nem jeleníthető meg szerkesztő módra való váltáskor. A lekérdezés egyes részei elveszhetnek.",
"confirmText-continue": "Folytatás",
"kick-start-your-query": "Előbeállításos lekérdezés",
"label-explain": "Magyarázat",
"run-queries": "Lekérdezések futtatása",
"title-parsing-error-switch-builder": "Elemzési hiba: Váltás építő módba?"
},
"prom-query-legend-editor": {
"aria-label-legend": "Jelmagyarázat kombinált listája",
"label-legend": "Jelmagyarázat",
"placeholder-select-legend-mode": "Jelmagyarázat mód kiválasztása",
"tooltip-legend": "Sorozatnév felülbírálása vagy sablon. Például a(z) {{templateExample}} helyébe a(z) {{labelName}} címkeértéke lép."
},
"query-builder-hints": {
"hint-details": "tipp: {{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "Építő",
"label-code": "Kód"
}
},
"query-pattern": {
"apply-query": "Lekérdezés alkalmazása",
"aria-label-apply-query-starter-button": "lekérdezés-előbeállító alkalmazása gomb",
"aria-label-back-button": "vissza gomb",
"aria-label-create-new-query-button": "új lekérdezés létrehozása gomb",
"aria-label-raw-query": "{{patternName}} nyers lekérdezés",
"aria-label-use-this-query-button": "használja ezt a lekérdezést gomb",
"back": "Vissza",
"create-new-query": "Új lekérdezés létrehozása",
"use-this-query": "Használja ezt a lekérdezést"
},
"query-patterns-modal": {
"aria-label-close-kick-start-your-query-modal": "előbeállításos lekérdezés modális ablakának bezárása",
"aria-label-kick-start-your-query-modal": "Előbeállításos lekérdezés modális ablak",
"aria-label-toggle-query-starter": "{{patternType}} lekérdezés-előbeállító kártya megnyitása és bezárása",
"close": "Bezárás",
"description-kick-start-your-query": "Kezdje el létrehozni a lekérdezést előbeállításokkal az alábbi lekérdezések egyikének kiválasztásával. Ezután folytathatja a lekérdezés befejezését.",
"label-toggle-query-starter": "{{patternType}} lekérdezés-előbeállítók",
"title-kick-start-your-query": "Előbeállításos lekérdezés"
},
"raw-query": {
"aria-label-selector": "választó"
},
"results-table": {
"content-descriptive-type": "{{descriptiveType}} létrehozásakor a Prometheus több, számláló típusú sorozatot jelenít meg. ",
"description": "Leírás",
"message-expand-label-filters": "Nem található metrika. Próbálja meg kibővíteni a címkeszűrőket.",
"message-expand-search": "Nem található metrika. Próbálja meg kibővíteni a keresést és a szűrőket.",
"message-no-metrics-found": "Nem található metrika az adatforrásban.",
"name": "Név",
"type": "Típus"
}
}
}
}

View File

@@ -1,492 +0,0 @@
{
"grafana-prometheus": {
"components": {
"annotation-query-editor": {
"annotation-data-load-error": "Terjadi kesalahan pemuatan data anotasi!",
"aria-label-lower-limit-parameter": "Tetapkan batas bawah untuk parameter langkah",
"label-min-step": "Langkah minimum",
"label-series-value-as-timestamp": "Nilai data seri sebagai stempel waktu",
"label-tags": "Tag",
"label-text": "Teks",
"label-title": "Judul",
"placeholder-auto": "otomatis",
"tooltip-either-pattern-example-instance-replaced-label": "Gunakan nama atau pola. Misalnya, {{labelTemplate}} diganti dengan nilai label untuk label {{labelName}}.",
"tooltip-min-step": "Batas bawah tambahan untuk parameter langkah kueri Prometheus dan untuk variabel <2>{{intervalVar}}</2> dan <4>{{rateIntervalVar}}</4>.",
"tooltip-timestamp-milliseconds-series-value-seconds-multiply": "Unit stempel waktu adalah milidetik. Jika satuan nilai data seri adalah detik, kalikan vektor rentangnya dengan 1000."
},
"get-query-type-options": {
"description": {
"instant-query-range": "Jalankan kueri Instan dan kueri Rentang"
},
"label": {
"both": "Keduanya"
},
"range-options": {
"description": {
"query-range": "Jalankan kueri selama rentang waktu tertentu"
},
"label": {
"instant": "Instan",
"range": "Rentang"
}
}
},
"label-selector": {
"aria-label-filter-expression-for-label": "Filter persamaan berdasarkan label",
"description-select-labels": "Setelah nilai label dipilih, hanya kombinasi label yang mungkin yang ditampilkan.",
"select-labels-to-search-in": "2. Pilih label untuk pencarian"
},
"metric-selector": {
"aria-label-filter-expression-for-metric": "Filter persamaan berdasarkan metrik",
"aria-label-limit-results-from-series-endpoint": "Batasi hasil dari titik akhir data seri",
"description-series-limit": "Batas ini berlaku untuk semua metrik, label, dan nilai. Biarkan bidang kosong untuk menggunakan batas default. Atur ke 0 untuk menonaktifkan batas dan mengambil semuanya — ini dapat menyebabkan masalah kinerja.",
"label-select-metric": "Setelah metrik dipilih, hanya label yang mungkin yang ditampilkan. Label dibatasi oleh batas data seri di bawah ini.",
"select-a-metric": "1. Pilih metrik",
"series-limit": "Batas data seri"
},
"prom-cheat-sheet": {
"prom-ql-cheat-sheet": "Lembar Ringkasan PromQL"
},
"prom-exemplar-field": {
"exemplars": "Contoh",
"tooltip-disable-query": "Nonaktifkan kueri dengan contoh",
"tooltip-enable-query": "Aktifkan kueri dengan contoh"
},
"prom-explore-extra-field": {
"aria-label-prometheus-extra-field": "Bidang tambahan Prometheus",
"aria-label-query-type-field": "Bidang jenis kueri",
"aria-label-step-field": "Bidang langkah",
"min-step": "Langkah minimum",
"query-type": "Jenis kueri",
"tooltip-units-builtin-variables-example-interval-rateinterval": "Unit waktu dan variabel bawaan dapat digunakan di sini, misalnya: {{example1}}, {{example2}}, {{example3}}, {{example4}}, {{example5}}, {{example6}}, {{example7}} (Default jika tidak ada unit yang ditentukan: {{default}})"
},
"prom-query-field": {
"placeholder-enter-a-prom-ql-query": "Masukkan kueri PromQL…"
},
"prom-variable-query-editor": {
"aria-label-classic-query": "Kueri Klasik",
"aria-label-metric-regex": "Ekspresi reguler metrik",
"aria-label-metric-selector": "Selektor metrik",
"aria-label-prometheus-query": "Kueri Prometheus",
"aria-label-query-type": "Jenis kueri",
"aria-label-series-query": "Kueri Data Seri",
"label-classic-query": "Kueri Klasik",
"label-label": "Label",
"label-metric-regex": "Ekspresi reguler metrik",
"label-query": "Kueri",
"label-query-type": "Jenis kueri",
"label-series-query": "Kueri Data Seri",
"placeholder-classic-query": "Kueri Klasik",
"placeholder-metric-regex": "Ekspresi reguler metrik",
"placeholder-prometheus-query": "Kueri Prometheus",
"placeholder-select-query-type": "Pilih jenis kueri",
"placeholder-series-query": "Kueri Data Seri",
"returns-metrics-matching-specified-metric-regex": "Mengembalikan daftar metrik yang sesuai dengan regex metrik yang ditentukan.",
"tooltip-classic-query": "Implementasi awal dari editor kueri variabel Prometheus. Masukkan string dengan jenis kueri dan parameter yang benar seperti yang dijelaskan dalam dokumen ini. Misalnya, {{exampleQuery}}.",
"tooltip-label": "Mengembalikan daftar nilai label untuk nama label di semua metrik kecuali metrik tersebut ditentukan.",
"tooltip-metric-regex": "Mengembalikan daftar nama label, secara opsional menyaring berdasarkan regex metrik yang ditentukan.",
"tooltip-query": "Mengembalikan daftar hasil kueri Prometheus untuk kueri tersebut. Ini dapat mencakup fungsi Prometheus, yaitu {{exampleQuery}}.",
"tooltip-query-type": "Plugin sumber data Prometheus menyediakan jenis kueri berikut untuk variabel templat.",
"tooltip-series-query": "Masukkan metrik dengan label, hanya metrik atau hanya label, yaitu {{example1}}, {{example2}}, atau {{example3}}. Mengembalikan daftar deret waktu yang terkait dengan data yang dimasukkan."
},
"selector-actions": {
"aria-label-selector": "selektor",
"aria-label-selector-clear-button": "Tombol hapus selektor",
"aria-label-use-selector-as-metrics-button": "Gunakan selektor sebagai tombol metrik",
"aria-label-use-selector-for-query-button": "Gunakan selektor untuk tombol kueri",
"aria-label-validate-submit-button": "Validasi tombol kirim",
"clear": "Hapus",
"resulting-selector": "4. Selektor yang dihasilkan",
"use-as-rate-query": "Gunakan sebagai kueri rate",
"use-query": "Gunakan kueri",
"validate-selector": "Validasi selektor"
},
"value-selector": {
"aria-label-filter-expression-for-label-values": "Filter persamaan berdasarkan nilai label",
"aria-label-values-for": "Nilai untuk {{labelKey}}",
"description-search-field-values-across-selected-labels": "Gunakan bidang pencarian untuk menemukan nilai di seluruh label yang dipilih.",
"select-multiple-values-for-your-labels": "3. Pilih (beberapa) nilai untuk label Anda"
}
},
"configuration": {
"alerting-settings-overhaul": {
"label-allow-as-recording-rules-target": "Izinkan sebagai target aturan perekaman",
"label-manage-alerts-via-alerting-ui": "Kelola peringatan melalui UI Peringatan",
"title-alerting": "Alerting",
"tooltip-allow-as-recording-rules-target": "Izinkan sumber data ini dipilih sebagai target untuk menulis aturan perekaman.",
"tooltip-manage-alerts-via-alerting-ui": "Kelola aturan peringatan untuk sumber data ini. Untuk mengelola sumber daya alerting lainnya, tambahkan sumber data Alertmanager."
},
"config-editor": {
"browser-access-mode-error": "Mode akses browser di sumber data Prometheus tidak lagi tersedia. Beralih ke mode akses server.",
"description-advanced-settings": "Pengaturan tambahan adalah pengaturan opsional yang dapat dikonfigurasi agar memiliki kendali lebih besar atas sumber data Anda.",
"title-advanced-settings": "Pengaturan lanjutan",
"title-error": "Kesalahan"
},
"data-source-http-settings-overhaul": {
"tooltip-browser-access-mode": "Metode akses Anda adalah <1>Browser</1>, ini berarti URL harus dapat diakses dari browser.",
"tooltip-http-url": "Tentukan URL HTTP lengkap (misalnya {{exampleURL}})",
"tooltip-server-access-mode": "Metode akses Anda adalah <1>Server</1>, ini berarti URL harus dapat diakses dari backend/server grafana."
},
"docs-tip": {
"visit-docs-for-more-details-here": "Kunjungi dokumen untuk detail selengkapnya di sini."
},
"exemplar-setting": {
"label-data-source": "Sumber data",
"label-internal-link": "Tautan internal",
"label-label-name": "Nama label",
"label-remove-exemplar-link": "Hapus tautan contoh",
"label-url": "URL",
"label-url-label": "Label URL",
"placeholder-go-to-examplecom": "Buka example.com",
"placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}",
"placeholder-trace-id": "traceID",
"title-remove-exemplar-link": "Hapus tautan contoh",
"tooltip-data-source": "Sumber data yang akan dinavigasi oleh contoh.",
"tooltip-internal-link": "Aktifkan opsi ini jika Anda memiliki tautan internal. Saat diaktifkan, ini akan menampilkan selektor sumber data. Pilih penyimpanan data pelacakan backend untuk data contoh Anda.",
"tooltip-label-name": "Nama bidang dalam objek label yang harus digunakan untuk mendapatkan traceID.",
"tooltip-url": "URL backend jejak yang akan digunakan pengguna untuk melihat jejaknya",
"tooltip-url-label": "Gunakan untuk mengganti label tombol pada bidang traceID contoh."
},
"exemplars-settings": {
"add": "Tambahkan",
"no-exemplars-configurations": "Tidak ada konfigurasi contoh",
"title-exemplars": "Contoh"
},
"prom-settings": {
"aria-label-default-editor": "Editor Default (Kode atau Pembuat)",
"aria-label-prom-type-type": "Jenis {{promType}}",
"aria-label-prometheus-type": "Jenis Prometheus",
"aria-label-select-http-method": "Pilih metode HTTP",
"editor-options": {
"label-builder": "Pembangun",
"label-code": "Kode"
},
"label-cache-level": "Level cache",
"label-custom-query-parameters": "Parameter kueri kustom",
"label-default-editor": "Editor default",
"label-disable-metrics-lookup": "Nonaktifkan pencarian metrik",
"label-disable-recording-rules-beta": "Nonaktifkan aturan perekaman (beta)",
"label-http-method": "Metode HTTP",
"label-incremental-querying-beta": "Kueri inkremental (beta)",
"label-prom-type-version": "Versi {{promType}}",
"label-prometheus-type": "Jenis Prometheus",
"label-query-overlap-window": "Jendela kueri tumpang tindih",
"label-query-timeout": "Batas waktu kueri habis",
"label-scrape-interval": "Interval scrape",
"label-series-limit": "Batas data seri",
"label-use-series-endpoint": "Gunakan titik akhir data seri",
"more-info": "Untuk informasi lengkap tentang mengonfigurasi jenis dan versi prometheus dalam sumber data, lihat <2>dokumentasi penyediaan</2>.",
"placeholder-example-maxsourceresolutionmtimeout": "Contoh: {{example}}",
"title-interval-behaviour": "Perilaku interval",
"title-other": "Lainnya",
"title-performance": "Kinerja",
"title-query-editor": "Editor kueri",
"tooltip-cache-level": "Mengatur level cache peramban untuk kueri editor. Pengaturan cache yang lebih tinggi disarankan untuk sumber data dengan kardinalitas tinggi.",
"tooltip-custom-query-parameters": "Tambahkan parameter kustom ke URL kueri Prometheus. Misalnya {{example1}}, {{example2}}, {{example3}}, atau {{example4}}. Beberapa parameter harus digabungkan bersama dengan {{concatenationChar}}.",
"tooltip-default-editor": "Atur opsi editor default untuk semua pengguna sumber data ini.",
"tooltip-disable-metrics-lookup": "Memeriksa opsi ini akan menonaktifkan pemilih metrik dan dukungan metrik/label dalam pelengkapan otomatis bidang kueri. Ini membantu jika Anda memiliki masalah kinerja dengan instans Prometheus yang lebih besar. ",
"tooltip-disable-recording-rules-beta": "Fitur ini akan menonaktifkan aturan perekaman. Aktifkan ini untuk meningkatkan kinerja dasbor",
"tooltip-http-method": "Anda dapat menggunakan metode HTTP POST atau GET untuk melakukan kueri sumber data Prometheus Anda. POST adalah metode yang disarankan karena memungkinkan kueri yang lebih besar. Ubah ini menjadi GET jika Anda memiliki versi Prometheus yang lebih lama dari 2.1 atau jika permintaan POST dibatasi di jaringan Anda.",
"tooltip-incremental-querying-beta": "Fitur ini akan mengubah perilaku default kueri relatif: alih-alih selalu meminta data terbaru dari instance Prometheus, hasil kueri akan disimpan sementara, dan hanya data baru yang akan diminta. Aktifkan ini untuk mengurangi load database dan jaringan.",
"tooltip-prom-type-version": "Gunakan ini untuk mengatur versi instance {{promType}} Anda jika tidak dikonfigurasi secara otomatis.",
"tooltip-prometheus-type": "Atur ini ke jenis database prometheus Anda, misalnya Prometheus, Cortex, Mimir, atau Thanos. Mengubah bidang ini akan menyimpan pengaturan Anda saat ini. Jenis Prometheus tertentu mendukung atau tidak mendukung berbagai API. Misalnya, beberapa jenis mendukung pencocokan regex bagi kueri label untuk meningkatkan kinerja. Beberapa jenis memiliki API untuk metadata. Jika Anda mengatur ini secara tidak benar, Anda mungkin mengalami perilaku tidak biasa saat melakukan kueri metrik dan label. Periksa dokumentasi Prometheus Anda untuk memastikan Anda memasukkan jenis yang benar.",
"tooltip-query-overlap-window": "Tetapkan durasi seperti {{example1}} atau {{example2}} atau {{example3}}. Default {{default}}. Durasi ini akan ditambahkan ke durasi setiap permintaan tambahan.",
"tooltip-query-timeout": "Atur batas waktu kueri Prometheus.",
"tooltip-scrape-interval": "Interval ini adalah seberapa sering Prometheus melakukan scrape pada target. Atur ini ke interval scrape dan evaluasi umum yang dikonfigurasi dalam file konfigurasi Prometheus Anda. Jika Anda mengatur ini ke nilai yang lebih besar dari interval file konfigurasi Prometheus Anda, Grafana akan mengevaluasi data sesuai dengan interval ini dan Anda akan melihat lebih sedikit titik data. Default ke {{default}}.",
"tooltip-series-limit": "Batas ini berlaku untuk semua sumber daya (metrik, label, dan nilai) untuk kedua endpoint (deret dan label). Biarkan bidang kosong untuk menggunakan batas default (40000). Atur ke 0 untuk menonaktifkan batas dan mengambil semuanya — ini dapat menyebabkan masalah kinerja. Batas default adalah 40000.",
"tooltip-use-series-endpoint": "Memeriksa opsi ini akan mengutamakan titik akhir data seri dengan {{exampleParameter}} parameter daripada titik akhir nilai label dengan {{exampleParameter}} parameter. Meskipun titik akhir nilai label dianggap memiliki kinerja yang lebih baik, beberapa pengguna mungkin lebih menyukai data seri karena memiliki metode POST, sementara titik akhir nilai label hanya memiliki metode GET."
}
},
"metrics-browser": {
"disabled-label": "(Dinonaktifkan)",
"enabled-label": "Browser metrik"
},
"prom-query-legend-editor": {
"get-legend-mode-options": {
"description-auto": "Hanya menyertakan label unik",
"description-custom": "Berikan templat penamaan",
"description-verbose": "Semua nama dan nilai label",
"label-auto": "Otomatis",
"label-custom": "Kustom",
"label-verbose": "Verbose"
}
},
"promql": {
"getAggregationOptions": {
"documentation-avg": "Hitung rata-rata atas dimensi",
"documentation-bottomk": "Elemen k terkecil berdasarkan nilai sampel",
"documentation-count": "Hitung jumlah elemen dalam vektor",
"documentation-count-values": "Hitung jumlah elemen dengan nilai yang sama",
"documentation-group": "Semua nilai dalam vektor yang dihasilkan adalah 1",
"documentation-max": "Pilih dimensi maksimum",
"documentation-min": "Pilih dimensi minimum",
"documentation-quantile": "Hitung φ-kuantil (0 ≤ φ ≤ 1) berdasarkan dimensi",
"documentation-stddev": "Hitung standar deviasi populasi berdasarkan dimensi",
"documentation-stdvar": "Hitung varians standar populasi berdasarkan dimensi",
"documentation-sum": "Hitung jumlah berdasarkan dimensi",
"documentation-topk": "Elemen k terbesar berdasarkan nilai sampel"
},
"getFunctions": {
"documentation-abs": "Mengembalikan vektor input dengan semua nilai sampel dikonversi ke nilai absolutnya.",
"documentation-absent": "Mengembalikan vektor kosong jika vektor yang diteruskan memiliki elemen, dan vektor dengan satu elemen bernilai 1 jika vektor tersebut tidak memiliki elemen. Ini berguna untuk memberi peringatan saat tidak ada deret waktu yang tersedia untuk kombinasi nama metrik dan label tertentu.",
"documentation-absent-over-time": "Mengembalikan vektor kosong jika vektor rentang yang diteruskan memiliki elemen, dan vektor dengan satu elemen bernilai 1 jika vektor rentang tersebut tidak memiliki elemen.",
"documentation-avg-over-time": "Nilai rata-rata dari semua poin dalam interval yang ditentukan.",
"documentation-ceil": "Membulatkan nilai sampel dari semua elemen dalam `v` ke bilangan bulat terdekat.",
"documentation-changes": "Untuk setiap deret waktu input, `changes(v range-vector)` mengembalikan berapa kali nilainya telah berubah dalam rentang waktu yang diberikan sebagai vektor instan.",
"documentation-clamp": "Menjepit nilai sampel dari semua elemen dalam `v` untuk memiliki batas bawah `min` dan batas atas `max`.",
"documentation-clamp-max": "Menjepit nilai sampel dari semua elemen dalam `v` agar memiliki batas atas `maks`.",
"documentation-clamp-min": "Menjepit nilai sampel dari semua elemen dalam `v` untuk memiliki batas bawah `min`.",
"documentation-count-over-time": "Hitungan semua nilai dalam interval yang ditentukan.",
"documentation-count-scalar": "Mengembalikan jumlah elemen dalam vektor deret waktu sebagai skalar. Ini bertentangan dengan operator agregasi `count()`, yang selalu mengembalikan vektor (kosong jika vektor input kosong) dan memungkinkan pengelompokan berdasarkan label melalui klausa `by`.",
"documentation-day-of-month": "Mengembalikan hari dalam sebulan untuk setiap waktu yang diberikan dalam UTC. Nilai yang dikembalikan adalah dari 1 hingga 31.",
"documentation-day-of-week": "Mengembalikan hari dalam seminggu untuk setiap waktu yang diberikan dalam UTC. Nilai yang dikembalikan adalah dari 0 hingga 6, di mana 0 berarti hari Minggu, dll.",
"documentation-day-of-year": "Mengembalikan hari dalam setahun untuk setiap waktu yang diberikan dalam UTC. Nilai yang dikembalikan adalah dari 1 hingga 365 untuk tahun non-kabisat, dan 1 hingga 366 pada tahun kabisat.",
"documentation-days-in-month": "Mengembalikan jumlah hari dalam sebulan untuk setiap waktu yang diberikan dalam UTC. Nilai yang dikembalikan adalah dari 28 hingga 31.",
"documentation-deg": "Mengonversi radian ke derajat untuk semua elemen dalam v",
"documentation-delta": "Menghitung perbedaan antara nilai pertama dan terakhir dari setiap elemen deret waktu dalam vektor rentang `v`, lalu mengembalikan vektor instan dengan delta dan label yang sesuai. Delta tersebut diekstrapolasi untuk mencakup seluruh rentang waktu yang ditentukan oleh pemilih vektor, sehingga memungkinkan hasil berupa bilangan non-integer meskipun nilai sampelnya semuanya bilangan bulat.",
"documentation-deriv": "Menghitung turunan per detik dari deret waktu dalam vektor rentang `v`, menggunakan regresi linier sederhana.",
"documentation-double-exponential-smoothing": "Menghasilkan nilai yang dihaluskan untuk deret waktu berdasarkan rentang dalam `v`. Semakin rendah faktor penghalusan `sf`, semakin besar bobot yang diberikan ke data lama. Semakin tinggi faktor tren `tf`, semakin banyak tren dalam data yang dipertimbangkan. `sf` dan `tf` harus antara 0 dan 1.",
"documentation-drop-common-labels": "Menghapus semua label yang memiliki nama dan nilai yang sama di semua deret dalam vektor input.",
"documentation-exp": "Menghitung fungsi eksponensial untuk semua elemen dalam `v`.\nKasus khusus adalah:\n* `Exp(+Inf) = +Inf` \n* `Exp(NaN) = NaN`",
"documentation-floor": "Membulatkan nilai sampel dari semua elemen dalam `v` ke bilangan bulat terdekat.",
"documentation-histogram-avg": "Mengembalikan rata-rata aritmatika dari nilai yang diamati yang disimpan dalam histogram asli. Sampel yang bukan histogram asli diabaikan dan tidak muncul dalam vektor yang dikembalikan.",
"documentation-histogram-count": "Mengembalikan jumlah pengamatan yang disimpan dalam histogram asli.",
"documentation-histogram-fraction": "Mengembalikan perkiraan fraksi pengamatan antara nilai bawah dan atas yang diberikan.",
"documentation-histogram-quantile": "Menghitung φ-kuantil (0 ≤ φ ≤ 1) dari bucket `b` dari histogram. Sampel dalam 'b' adalah jumlah pengamatan di setiap bucket. Setiap sampel harus memiliki label `le` di mana nilai label menunjukkan batas atas inklusif dari bucket. (Sampel tanpa label seperti itu akan diabaikan tanpa peringatan.) Tipe metrik histogram secara otomatis menyediakan deret waktu dengan akhiran `_bucket` dan label yang sesuai.",
"documentation-histogram-stddev": "Mengembalikan estimasi standar deviasi pengamatan dalam histogram asli, berdasarkan rata-rata geometris dari bucket tempat pengamatan berada.",
"documentation-histogram-stdvar": "Mengembalikan perkiraan varians standar pengamatan dalam histogram asli.",
"documentation-histogram-sum": "Mengembalikan jumlah pengamatan yang disimpan dalam histogram asli.",
"documentation-holt-winters": "Diganti namanya menjadi double_exponential_smoothing di prometheus v3.x. Untuk versi prometheus yang sama dan lebih besar dari v3.0, gunakan double_exponential_smoothing. \n\nMenghasilkan nilai yang dihaluskan untuk deret waktu berdasarkan rentang dalam `v`. Semakin rendah faktor penghalusan `sf`, semakin besar bobot yang diberikan ke data lama. Semakin tinggi faktor tren `tf`, semakin banyak tren dalam data yang dipertimbangkan. `sf` dan `tf` harus antara 0 dan 1.",
"documentation-hour": "Mengembalikan jam dalam sehari untuk setiap waktu yang diberikan dalam UTC. Nilai yang dikembalikan adalah dari 0 hingga 23.",
"documentation-idelta": "Menghitung perbedaan antara dua sampel terakhir dalam vektor rentang `v`, mengembalikan vektor instan dengan delta dan label yang setara yang diberikan.",
"documentation-increase": "Menghitung peningkatan dalam deret waktu pada vektor rentang. Pemutusan dalam monotonisitas (seperti pengaturan ulang penghitung akibat target dimulai ulang) disesuaikan secara otomatis. Nilai peningkatan diekstrapolasi untuk mencakup seluruh rentang waktu seperti yang ditentukan dalam pemilih vektor rentang, sehingga memungkinkan diperolehnya hasil non-integer meskipun penghitung hanya bertambah dalam satuan bulat.",
"documentation-info": "Mengembalikan detail dan metadata terbaru tentang sekelompok metrik, seperti label dan nilai saat ini, tanpa melakukan perhitungan apa pun",
"documentation-irate": "Menghitung laju kenaikan instan per detik dari deret waktu dalam vektor rentang, berdasarkan dua titik data terakhir. Pemutusan dalam monotonisitas (seperti reset penghitung karena target dimulai ulang) disesuaikan secara otomatis.",
"documentation-label-join": "Untuk setiap deret waktu dalam `v`, gabungkan semua nilai dari semua `src_labels` menggunakan `separator` dan kembalikan rangkaian waktu dengan label `dst_label` yang berisi nilai yang digabungkan. Dalam fungsi ini, dapat ada sejumlah `src_labels`.",
"documentation-label-replace": "Untuk setiap deret waktu dalam `v`, `label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)` mencocokkan ekspresi reguler `regex` terhadap label `src_label`. Jika cocok, rangkaian waktu dikembalikan dengan label `dst_label` diganti dengan perluasan `replacement`. `$1` diganti dengan subkelompok pertama yang cocok, `$2` dengan yang kedua, dll. Jika ekspresi reguler tidak cocok, deret waktu dikembalikan tanpa perubahan.",
"documentation-last-over-time": "Nilai poin terbaru dalam interval yang ditentukan.",
"documentation-ln": "Menghitung logaritma alami untuk semua elemen dalam `v`.\nKasus khusus adalah:\n * `ln(+Inf) = +Inf`\n * `ln(0) = -Inf`\n * `ln(x < 0) = NaN`\n * `ln(NaN) = NaN`",
"documentation-log10": "Menghitung logaritma desimal untuk semua elemen dalam `v`. Kasus khusus setara dengan yang ada di `ln`.",
"documentation-log2": "Menghitung logaritma biner untuk semua elemen dalam `v`. Kasus khusus setara dengan yang ada di `ln`.",
"documentation-max-over-time": "Nilai maksimum dari semua poin dalam interval yang ditentukan.",
"documentation-min-over-time": "Nilai minimum dari semua poin dalam interval yang ditentukan.",
"documentation-minute": "Mengembalikan menit dari jam untuk setiap waktu yang diberikan dalam UTC. Nilai yang dikembalikan adalah dari 0 hingga 59.",
"documentation-month": "Mengembalikan bulan dalam setahun untuk setiap waktu yang diberikan dalam UTC. Nilai yang dikembalikan adalah dari 1 hingga 12, di mana 1 berarti Januari, dll.",
"documentation-pi": "Mengembalikan pi",
"documentation-predict-linear": "Memprediksi nilai deret waktu `t` detik dari sekarang, berdasarkan vektor rentang `v`, menggunakan regresi linier sederhana.",
"documentation-present-over-time": "Nilai 1 untuk setiap deret dalam interval yang ditentukan.",
"documentation-quantile-over-time": "Nilai φ-kuantil (0 ≤ φ ≤ 1) dalam interval yang ditentukan.",
"documentation-rad": "Mengonversi derajat menjadi radian untuk semua elemen dalam v",
"documentation-rate": "Menghitung laju peningkatan rata-rata per detik dari deret waktu dalam vektor rentang. Pemutusan monotonisitas (seperti reset penghitung akibat target yang dimulai ulang) akan disesuaikan secara otomatis. Selain itu, perhitungan ini juga melakukan ekstrapolasi hingga ke ujung rentang waktu, sehingga tetap memperhitungkan scrape yang terlewat atau ketidaksesuaian waktu antara siklus scrape dan periode waktu rentang.",
"documentation-resets": "Untuk setiap deret waktu input, `resets(v range-vector)` mengembalikan jumlah reset penghitung dalam rentang waktu yang diberikan sebagai vektor instan. Setiap penurunan nilai antara dua sampel berturut-turut ditafsirkan sebagai reset penghitung.",
"documentation-round": "Membulatkan nilai sampel dari semua elemen dalam `v` ke bilangan bulat terdekat. Ikatan diselesaikan dengan pembulatan ke atas. Argumen opsional `to_nearest` memungkinkan untuk menentukan kelipatan terdekat di mana nilai sampel harus dibulatkan. Kelipatan ini juga mungkin pecahan.",
"documentation-scalar": "Dengan vektor input elemen tunggal, `skalar(v instant-vector)` mengembalikan nilai sampel dari elemen tunggal tersebut sebagai skalar. Jika vektor input tidak memiliki satu elemen persis, `skalar` akan mengembalikan `NaN`.",
"documentation-sgn": "Mengembalikan vektor dengan semua nilai sampel dikonversi ke tanda mereka, didefinisikan sebagai berikut: 1 jika v positif, -1 jika v negatif dan 0 jika v sama dengan nol.",
"documentation-sort": "Mengembalikan elemen vektor yang diurutkan berdasarkan nilai sampelnya, dalam urutan naik.",
"documentation-sort-desc": "Mengembalikan elemen vektor yang diurutkan berdasarkan nilai sampelnya, dalam urutan menurun.",
"documentation-sqrt": "Menghitung akar kuadrat dari semua elemen dalam `v`.",
"documentation-stddev-over-time": "Standar deviasi populasi dari nilai dalam interval yang ditentukan.",
"documentation-stdvar-over-time": "Varians standar populasi dari nilai dalam interval yang ditentukan.",
"documentation-sum-over-time": "Jumlah semua nilai dalam interval yang ditentukan.",
"documentation-time": "Mengembalikan jumlah detik sejak 1 Januari 1970 UTC. Perhatikan bahwa ini tidak benar-benar mengembalikan waktu saat ini, tetapi waktu saat ekspresi dievaluasi.",
"documentation-timestamp": "Mengembalikan stempel waktu dari setiap sampel vektor yang diberikan sebagai jumlah detik sejak 1 Januari 1970 UTC.",
"documentation-vector": "Mengembalikan skalar `s` sebagai vektor tanpa label.",
"documentation-year": "Mengembalikan tahun untuk setiap waktu yang diberikan dalam UTC."
},
"getTrigonometricFunctions": {
"documentation-acos": "menghitung arkus kosinus dari semua elemen di {{argument}}",
"documentation-acosh": "menghitung kosinus hiperbolik inversi dari semua elemen di {{argument}}",
"documentation-asin": "menghitung arcsin dari semua elemen di {{argument}}",
"documentation-asinh": "menghitung sinus hiperbolik inversi dari semua elemen di {{argument}}",
"documentation-atan": "menghitung arctangent dari semua elemen di {{argument}}",
"documentation-atanh": "menghitung tanget hiperbolik inversi dari semua elemen di {{argument}}",
"documentation-cos": "menghitung kosinus dari semua elemen di {{argument}}",
"documentation-cosh": "menghitung kosinus hiperbolik dari semua elemen di {{argument}}",
"documentation-sin": "menghitung sinus dari semua elemen di {{argument}}",
"documentation-sinh": "menghitung sinus hiperbolik dari semua elemen di {{argument}}",
"documentation-tan": "menghitung tangent dari semua elemen di {{argument}}",
"documentation-tanh": "menghitung tanget hiperbolik dari semua elemen di {{argument}}"
}
},
"querybuilder": {
"feedback-link": {
"give-feedback": "Berikan umpan balik",
"title-give-feedback": "Penjelajah metrik ini masih baru, beri tahu kami cara untuk meningkatkannya"
},
"get-collapsed-info": {
"exemplars": "Contoh: {{value}}",
"format": "Format: {{value}}",
"legend": "Legenda: {{value}}",
"step": "Langkah: {{value}}",
"type": "Tipe: {{value}}"
},
"get-placeholders": {
"browse": "Cari metrik berdasarkan nama",
"type": "Filter berdasarkan jenis"
},
"get-prom-types": {
"description-counter": "Metrik kumulatif yang merepresentasikan satu penghitung yang meningkat secara monoton, yang nilainya hanya dapat bertambah atau direset ke nol saat sistem dimulai ulang.",
"description-gauge": "Metrik yang merepresentasikan satu nilai numerik yang dapat naik turun secara bebas.",
"description-histogram": "Histogram mengambil sampel pengamatan (biasanya seperti durasi permintaan atau ukuran respons) dan menghitungnya dalam bucket yang dapat dikonfigurasi.",
"description-native-histogram": "Histogram asli berbeda dari histogram Prometheus klasik dalam beberapa hal: Batas bucket pada histogram asli dihitung menggunakan rumus yang bergantung pada skala (resolusi) histogram, dan tidak ditentukan oleh pengguna.",
"description-no-type": "Metrik ini tidak memiliki tipe yang ditentukan dalam metadata.",
"description-summary": "Ringkasan mengambil sampel pengamatan (biasanya seperti durasi permintaan dan ukuran respons), dan dapat menghitung kuantil yang dapat dikonfigurasi selama rentang waktu geser.",
"description-unknown": "Metrik ini telah diberi tipe yang tidak diketahui dalam metadata.",
"label-counter": "Penghitung",
"label-gauge": "Alat ukur",
"label-histogram": "Histogram",
"label-native-histogram": "Histogram asli",
"label-no-type": "Tanpa tipe",
"label-summary": "Ringkasan",
"label-unknown": "Tidak diketahui"
},
"handle-function": {
"text": {
"query-parsing-is-ambiguous": "Penguraian kueri mengalami masalah ketidakjelasan."
}
},
"label-filter-item": {
"aria-label-remove": "Hapus {{name}}",
"placeholder-select-label": "Pilih label",
"placeholder-select-value": "Pilih nilai"
},
"label-filters": {
"label-filters": "Filter label",
"label-label-filters": "Filter label",
"tooltip-label-filters": "Opsional: digunakan untuk menyaring metrik yang dipilih untuk jenis kueri ini."
},
"label-param-editor": {
"loadingMessage-loading-labels": "Memuat label",
"noOptionsMessage-no-labels-found": "Tidak ada label yang ditemukan"
},
"metric-combobox": {
"async-select": {
"aria-label-open-metrics-explorer": "Buka penjelajah metrik",
"placeholder-select-metric": "Pilih metrik",
"tooltip-open-metrics-explorer": "Buka penjelajah metrik"
},
"label-metric": "Metrik",
"tooltip-metric": "Opsional: mengembalikan daftar nilai label untuk nama label dalam metrik yang ditentukan."
},
"metrics-modal": {
"aria-label-browse-metrics": "Telusuri metrik",
"currently-selected": "Sedang dipilih: {{selected}}",
"metrics-pre-filtered": "Metrik ini telah difilter sebelumnya berdasarkan label yang dipilih di filter label.",
"title-metrics-explorer": "Penjelajah metrik"
},
"nested-query": {
"label": {
"ignoring": "Mengabaikan",
"on": "Aktif"
},
"operator": "Operator",
"tooltip-remove-match": "Hapus kecocokan",
"vector-matches": "Kecocokan vektor"
},
"operation-editor": {
"not-found": "Operasi {{id}} tidak ditemukan",
"title-remove": "Hapus {{name}}"
},
"operation-header": {
"placeholder-replace-with": "Ganti dengan",
"title-click-to-view-alternative-operations": "Klik untuk melihat operasi alternatif",
"title-remove-operation": "Hapus operasi"
},
"operation-info-button": {
"title-click-to-show-description": "Klik untuk menampilkan deskripsi",
"title-remove-operation": "Hapus operasi"
},
"operation-list": {
"operations": "Operasi",
"placeholder-search": "Cari",
"title-add-operation": "Tambah operasi"
},
"operation-param-editor": {
"title-add": "Tambahkan {{name}}",
"title-remove": "Hapus {{name}}"
},
"operation-utils": {
"getAggregationExplainer": {
"label-by": "Menghitung {{aggregationName}} berdasarkan dimensi sekaligus mempertahankan {{labelWord}} {{labels}}.",
"label-default": "Menghitung {{aggregationName}} berdasarkan dimensi.",
"label-without": "Menghitung {{aggregationName}} berdasarkan dimensi {{labels}}. Semua label lainnya dipertahankan."
}
},
"prom-query-builder-options": {
"aria-label-exemplars": "Sakelar contoh.",
"aria-label-format": "Format kotak kombo",
"aria-label-lower-limit-parameter": "Kotak teks langkah minimum, tetapkan batas bawah untuk parameter langkah",
"aria-label-select-resolution": "Pilih resolusi",
"aria-label-type": "Ketik grup tombol radio",
"format-options": {
"label-heatmap": "Peta panas",
"label-table": "Tabel",
"label-time-series": "Deret waktu"
},
"label-exemplars": "Contoh",
"label-format": "Format",
"label-min-step": "Langkah minimum",
"label-resolution": "Resolusi",
"label-type": "Jenis",
"placeholder-auto": "otomatis",
"title-options": "Opsi",
"tooltip-min-step": "Batas bawah tambahan untuk parameter langkah kueri Prometheus dan untuk variabel <2>{{interval}}</2> dan <4>{{rateInterval}}</4>."
},
"prom-query-editor-selector": {
"body-syntax-error": "Ada kesalahan sintaksis, atau struktur kueri tidak dapat divisualisasikan saat beralih ke mode pembuat. Bagian dari kueri mungkin hilang.",
"confirmText-continue": "Lanjutkan",
"kick-start-your-query": "Mulai kueri Anda",
"label-explain": "Penjelasan",
"run-queries": "Jalankan kueri",
"title-parsing-error-switch-builder": "Terjadi kesalahan penguraian: Beralih ke mode pembuat?"
},
"prom-query-legend-editor": {
"aria-label-legend": "Kotak kombo legenda",
"label-legend": "Legenda",
"placeholder-select-legend-mode": "Pilih mode legenda",
"tooltip-legend": "Mengganti nama seri atau templat. Misalnya, {{templateExample}} akan diganti dengan nilai label untuk {{labelName}}."
},
"query-builder-hints": {
"hint-details": "petunjuk: {{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "Pembangun",
"label-code": "Kode"
}
},
"query-pattern": {
"apply-query": "Terapkan kueri",
"aria-label-apply-query-starter-button": "tombol mulai terapkan kueri",
"aria-label-back-button": "tombol kembali",
"aria-label-create-new-query-button": "tombol buat kueri baru",
"aria-label-raw-query": "Kueri {{patternName}} mentah",
"aria-label-use-this-query-button": "tombol gunakan kueri ini",
"back": "Kembali",
"create-new-query": "Buat kueri baru",
"use-this-query": "Gunakan kueri ini"
},
"query-patterns-modal": {
"aria-label-close-kick-start-your-query-modal": "tutup modal untuk memulai kueri Anda",
"aria-label-kick-start-your-query-modal": "Mulai modal kueri Anda",
"aria-label-toggle-query-starter": "buka dan tutup kartu pemula kueri {{patternType}}",
"close": "Tutup",
"description-kick-start-your-query": "Mulai kueri Anda dengan memilih salah satu kueri ini. Anda kemudian dapat melanjutkan untuk menyelesaikan kueri Anda.",
"label-toggle-query-starter": "kueri awal {{patternType}}",
"title-kick-start-your-query": "Mulai kueri Anda"
},
"raw-query": {
"aria-label-selector": "selektor"
},
"results-table": {
"content-descriptive-type": "Saat membuat {{descriptiveType}}, Prometheus mengekspos beberapa data seri dengan penghitung jenis. ",
"description": "Deskripsi",
"message-expand-label-filters": "Tidak ada metrik yang ditemukan. Coba perluas filter label Anda.",
"message-expand-search": "Tidak ada metrik yang ditemukan. Coba perluas pencarian dan filter Anda.",
"message-no-metrics-found": "Tidak ada metrik yang ditemukan di sumber data.",
"name": "Nama",
"type": "Jenis"
}
}
}
}

View File

@@ -1,492 +0,0 @@
{
"grafana-prometheus": {
"components": {
"annotation-query-editor": {
"annotation-data-load-error": "Errore di caricamento dei dati di annotazione!",
"aria-label-lower-limit-parameter": "Imposta il limite inferiore per il parametro passaggio",
"label-min-step": "Passaggio minimo",
"label-series-value-as-timestamp": "Valore della serie come marca temporale",
"label-tags": "Tag",
"label-text": "Testo",
"label-title": "Titolo",
"placeholder-auto": "automatico",
"tooltip-either-pattern-example-instance-replaced-label": "Usa il nome o un modello. Ad esempio, {{labelTemplate}} viene sostituito con il valore dell'etichetta per l'etichetta {{labelName}}.",
"tooltip-min-step": "Un ulteriore limite inferiore per il parametro passaggio della query Prometheus e per le variabili <2>{{intervalVar}}</2> e <4>{{rateIntervalVar}}</4>.",
"tooltip-timestamp-milliseconds-series-value-seconds-multiply": "L'unità di misura della marca temporale è millisecondi. Se l'unità di misura del valore della serie è secondi, moltiplica il suo vettore di intervallo per 1000."
},
"get-query-type-options": {
"description": {
"instant-query-range": "Esegui una query istantanea e una query di intervallo"
},
"label": {
"both": "Entrambe"
},
"range-options": {
"description": {
"query-range": "Esegui query su un intervallo di tempo"
},
"label": {
"instant": "Istantanea",
"range": "Intervallo"
}
}
},
"label-selector": {
"aria-label-filter-expression-for-label": "Filtra espressione per etichetta",
"description-select-labels": "Una volta selezionati i valori delle etichette, vengono mostrate solo le possibili combinazioni di etichette.",
"select-labels-to-search-in": "2. Seleziona le etichette in cui cercare"
},
"metric-selector": {
"aria-label-filter-expression-for-metric": "Filtra espressione per metrica",
"aria-label-limit-results-from-series-endpoint": "Limita i risultati dall'endpoint della serie",
"description-series-limit": "Il limite si applica a tutte le metriche, etichette e valori. Lascia il campo vuoto per utilizzare il limite predefinito. Imposta su 0 per disabilitare il limite e recuperare tutto: ciò potrebbe causare problemi di prestazioni.",
"label-select-metric": "Una volta selezionata una metrica, vengono mostrate solo le possibili etichette. Le etichette sono limitate dal limite della serie riportato di seguito.",
"select-a-metric": "1. Seleziona una metrica",
"series-limit": "Limite della serie"
},
"prom-cheat-sheet": {
"prom-ql-cheat-sheet": "Scheda di riferimento rapido PromQL"
},
"prom-exemplar-field": {
"exemplars": "Esempi",
"tooltip-disable-query": "Disabilita query con esempi",
"tooltip-enable-query": "Abilita query con esempi"
},
"prom-explore-extra-field": {
"aria-label-prometheus-extra-field": "Campo aggiuntivo Prometheus",
"aria-label-query-type-field": "Campo di tipo query",
"aria-label-step-field": "Campo passaggio",
"min-step": "Passaggio minimo",
"query-type": "Tipo di query",
"tooltip-units-builtin-variables-example-interval-rateinterval": "Qui è possibile utilizzare unità di tempo e variabili integrate, ad esempio: {{example1}}, {{example2}}, {{example3}}, {{example4}}, {{example5}}, {{example6}}, {{example7}} (impostazione predefinita se non è specificata alcuna unità: {{default}})"
},
"prom-query-field": {
"placeholder-enter-a-prom-ql-query": "Inserisci una query PromQL…"
},
"prom-variable-query-editor": {
"aria-label-classic-query": "Query classica",
"aria-label-metric-regex": "Espressione regolare metrica",
"aria-label-metric-selector": "Selettore metrica",
"aria-label-prometheus-query": "Query Prometheus",
"aria-label-query-type": "Tipo di query",
"aria-label-series-query": "Query della serie",
"label-classic-query": "Query classica",
"label-label": "Etichetta",
"label-metric-regex": "Espressione regolare metrica",
"label-query": "Query",
"label-query-type": "Tipo di query",
"label-series-query": "Query della serie",
"placeholder-classic-query": "Query classica",
"placeholder-metric-regex": "Espressione regolare metrica",
"placeholder-prometheus-query": "Query Prometheus",
"placeholder-select-query-type": "Seleziona il tipo di query",
"placeholder-series-query": "Query della serie",
"returns-metrics-matching-specified-metric-regex": "Restituisce un elenco di metriche corrispondenti all'espressione regolare metrica specificata.",
"tooltip-classic-query": "L'implementazione originale dell'editor di query variabile Prometheus. Inserisci una stringa con il tipo di query e i parametri corretti come descritto in questi documenti. Ad esempio, {{exampleQuery}}.",
"tooltip-label": "Restituisce un elenco di valori di etichetta per il nome dell'etichetta in tutte le metriche, a meno che la metrica non sia specificata.",
"tooltip-metric-regex": "Restituisce un elenco di nomi di etichette, filtrando facoltativamente in base all'espressione regolare metrica specificata.",
"tooltip-query": "Restituisce un elenco di risultati della query Prometheus per la query. Questo può includere funzioni Prometheus, ad esempio {{exampleQuery}}.",
"tooltip-query-type": "Il plugin dell'origine dati di Prometheus fornisce i seguenti tipi di query per le variabili del modello.",
"tooltip-series-query": "Inserisci una metrica con etichette, solo una metrica o solo etichette, ad esempio {{example1}}, {{example2}} o {{example3}}. Restituisce un elenco di serie temporali associate ai dati inseriti."
},
"selector-actions": {
"aria-label-selector": "selettore",
"aria-label-selector-clear-button": "Pulsante Cancella selettore",
"aria-label-use-selector-as-metrics-button": "Pulsante Usa selettore come metrica",
"aria-label-use-selector-for-query-button": "Pulsante Usa selettore per query",
"aria-label-validate-submit-button": "Pulsante Convalida invio",
"clear": "Cancella",
"resulting-selector": "4. Selettore risultante",
"use-as-rate-query": "Usa come query di valutazione",
"use-query": "Usa query",
"validate-selector": "Convalida selettore"
},
"value-selector": {
"aria-label-filter-expression-for-label-values": "Filtra espressione per valori etichetta",
"aria-label-values-for": "Valori per {{labelKey}}",
"description-search-field-values-across-selected-labels": "Utilizza il campo di ricerca per trovare i valori nelle etichette selezionate.",
"select-multiple-values-for-your-labels": "3. Seleziona (più) valori per le tue etichette"
}
},
"configuration": {
"alerting-settings-overhaul": {
"label-allow-as-recording-rules-target": "Consenti come destinazione delle regole di registrazione",
"label-manage-alerts-via-alerting-ui": "Gestisci gli avvisi tramite l'interfaccia utente di avviso",
"title-alerting": "Avvisi",
"tooltip-allow-as-recording-rules-target": "Consenti a questa origine dati di essere selezionata come destinazione per la scrittura delle regole di registrazione.",
"tooltip-manage-alerts-via-alerting-ui": "Gestisci le regole di avviso per questa origine dati. Per gestire altre risorse di avviso, aggiungi un'origine dati Alertmanager."
},
"config-editor": {
"browser-access-mode-error": "La modalità di accesso al browser nell'origine dati di Prometheus non è più disponibile. Passa alla modalità di accesso al server.",
"description-advanced-settings": "Le impostazioni aggiuntive sono opzionali e possono essere configurate per un maggiore controllo sull'origine dei dati.",
"title-advanced-settings": "Impostazioni avanzate",
"title-error": "Errore"
},
"data-source-http-settings-overhaul": {
"tooltip-browser-access-mode": "Il metodo di accesso è <1>Browser</1>, quindi l'URL deve essere accessibile dal browser.",
"tooltip-http-url": "Specifica un URL HTTP completo (ad esempio {{exampleURL}})",
"tooltip-server-access-mode": "Il metodo di accesso è <1>Server</1>, quindi l'URL deve essere accessibile dal server/back-end di Grafana."
},
"docs-tip": {
"visit-docs-for-more-details-here": "Visita la documentazione per maggiori dettagli qui."
},
"exemplar-setting": {
"label-data-source": "Origine dei dati",
"label-internal-link": "Link interno",
"label-label-name": "Nome etichetta",
"label-remove-exemplar-link": "Rimuovi link di esempio",
"label-url": "URL",
"label-url-label": "Etichetta URL",
"placeholder-go-to-examplecom": "Vai a example.com",
"placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}",
"placeholder-trace-id": "traceID",
"title-remove-exemplar-link": "Rimuovi link di esempio",
"tooltip-data-source": "L'origine dati a cui l'esempio si collegherà.",
"tooltip-internal-link": "Abilita questa opzione se disponi di un link interno. Quando è abilitata, questa opzione mostra il selettore dell'origine dati. Seleziona l'archivio dati di tracciamento di backend per i dati dell'esempio.",
"tooltip-label-name": "Il nome del campo nell'oggetto etichette che deve essere utilizzato per ottenere il traceID.",
"tooltip-url": "L'URL di backend della traccia a cui l'utente verrebbe indirizzato per visualizzarla",
"tooltip-url-label": "Da utilizzare per sovrascrivere l'etichetta del pulsante sul campo traceID dell'esempio."
},
"exemplars-settings": {
"add": "Aggiungi",
"no-exemplars-configurations": "Nessuna configurazione di esempi",
"title-exemplars": "Esemplari"
},
"prom-settings": {
"aria-label-default-editor": "Editor predefinito (codice o generatore)",
"aria-label-prom-type-type": "Tipo {{promType}}",
"aria-label-prometheus-type": "Tipo Prometheus",
"aria-label-select-http-method": "Seleziona il metodo HTTP",
"editor-options": {
"label-builder": "Builder",
"label-code": "Codice"
},
"label-cache-level": "Livello cache",
"label-custom-query-parameters": "Parametri di query personalizzati",
"label-default-editor": "Editor predefinito",
"label-disable-metrics-lookup": "Disabilita ricerca delle metriche",
"label-disable-recording-rules-beta": "Disabilita regole di registrazione (beta)",
"label-http-method": "Metodo HTTP",
"label-incremental-querying-beta": "Query incrementale (beta)",
"label-prom-type-version": "Versione {{promType}}",
"label-prometheus-type": "Tipo Prometheus",
"label-query-overlap-window": "Finestra di sovrapposizione delle query",
"label-query-timeout": "Timeout query",
"label-scrape-interval": "Intervallo di raccolta",
"label-series-limit": "Limite della serie",
"label-use-series-endpoint": "Usa endpoint della serie",
"more-info": "Per ulteriori informazioni sulla configurazione del tipo e della versione di Prometheus nelle origini dati, consulta la <2>documentazione sul provisioning</2>.",
"placeholder-example-maxsourceresolutionmtimeout": "Esempio: {{example}}",
"title-interval-behaviour": "Comportamento intervallo",
"title-other": "Altro",
"title-performance": "Performance",
"title-query-editor": "Editor query",
"tooltip-cache-level": "Imposta il livello di memorizzazione nella cache del browser per le query dell'editor. Si consigliano impostazioni della cache più elevate per origini dati ad alta cardinalità.",
"tooltip-custom-query-parameters": "Aggiungi parametri personalizzati all'URL della query Prometheus. Ad esempio {{example1}}, {{example2}}, {{example3}} o {{example4}}. Più parametri devono essere concatenati insieme a {{concatenationChar}}.",
"tooltip-default-editor": "Imposta l'opzione editor predefinita per tutti gli utenti di questa origine dati.",
"tooltip-disable-metrics-lookup": "Selezionando questa opzione, il selettore di metriche e il supporto di metriche/etichette nel completamento automatico del campo di query verranno disabilitati. Questo è utile se hai problemi di prestazioni con istanze Prometheus più grandi. ",
"tooltip-disable-recording-rules-beta": "Questa funzionalità disabiliterà le regole di registrazione. Attiva questa opzione per migliorare le prestazioni della dashboard",
"tooltip-http-method": "Puoi utilizzare il metodo POST o GET HTTP per eseguire query sull'origine dati di Prometheus. POST è il metodo consigliato in quanto consente query più grandi. Usa GET se hai una versione di Prometheus precedente alla 2.1 o se le richieste POST sono limitate nella tua rete.",
"tooltip-incremental-querying-beta": "Questa funzionalità modificherà il comportamento predefinito delle query relative per richiedere sempre dati aggiornati dall'istanza di Prometheus, mentre i risultati delle query verranno memorizzati nella cache e verranno richiesti solo i nuovi record. Attiva questa opzione per ridurre il carico del database e della rete.",
"tooltip-prom-type-version": "Utilizza questa opzione per impostare la versione della tua istanza {{promType}} se non è configurata automaticamente.",
"tooltip-prometheus-type": "Imposta questo valore sul tipo del tuo database Prometheus, ad esempio Prometheus, Cortex, Mimir o Thanos. La modifica di questo campo salverà le impostazioni correnti. Alcuni tipi di Prometheus supportano o meno varie API. Ad esempio, alcuni tipi supportano la corrispondenza delle espressioni regolari per le query delle etichette per migliorare le prestazioni. Alcuni tipi hanno un'API per i metadati. Se imposti questo valore in modo errato, potresti riscontrare un comportamento strano durante l'interrogazione di metriche ed etichette. Controlla la documentazione di Prometheus per assicurarti di inserire il tipo corretto.",
"tooltip-query-overlap-window": "Imposta una durata come {{example1}} o {{example2}} o {{example3}}. Impostazione predefinita di {{default}}. Questa durata verrà aggiunta alla durata di ogni richiesta incrementale.",
"tooltip-query-timeout": "Imposta il timeout della query Prometheus.",
"tooltip-scrape-interval": "Questo intervallo indica la frequenza con cui Prometheus esegue la raccolta dei target. Imposta questo valore sull'intervallo di raccolta e valutazione tipico configurato nel file di configurazione di Prometheus. Se imposti questo valore su un valore maggiore dell'intervallo del file di configurazione di Prometheus, Grafana valuterà i dati in base a questo intervallo e vedrai meno punti dati. L'impostazione predefinita è {{default}}.",
"tooltip-series-limit": "Il limite si applica a tutte le risorse (metriche, etichette e valori) per entrambi gli endpoint (serie ed etichette). Lascia il campo vuoto per utilizzare il limite predefinito (40000). Imposta su 0 per disabilitare il limite e recuperare tutto: ciò potrebbe causare problemi di prestazioni. Il limite predefinito è 40000.",
"tooltip-use-series-endpoint": "Selezionando questa opzione, l'endpoint della serie con il parametro {{exampleParameter}} sarà preferito rispetto all'endpoint dei valori dell'etichetta con il parametro {{exampleParameter}}. Sebbene l'endpoint dei valori dell'etichetta sia considerato più performante, alcuni utenti potrebbero preferire la serie perché ha un metodo POST mentre l'endpoint dei valori dell'etichetta ha solo un metodo GET."
}
},
"metrics-browser": {
"disabled-label": "(Disattivato)",
"enabled-label": "Browser metriche"
},
"prom-query-legend-editor": {
"get-legend-mode-options": {
"description-auto": "Include solo etichette univoche",
"description-custom": "Fornisci un modello di denominazione",
"description-verbose": "Tutti i nomi e i valori delle etichette",
"label-auto": "Automatico",
"label-custom": "Personalizzazione",
"label-verbose": "Dettagliata"
}
},
"promql": {
"getAggregationOptions": {
"documentation-avg": "Calcola la media sulle dimensioni",
"documentation-bottomk": "Elementi k più piccoli per valore campione",
"documentation-count": "Conta il numero di elementi nel vettore",
"documentation-count-values": "Conta il numero di elementi con lo stesso valore",
"documentation-group": "Tutti i valori nel vettore risultante sono 1",
"documentation-max": "Seleziona massimo su dimensioni",
"documentation-min": "Seleziona minimo su dimensioni",
"documentation-quantile": "Calcola φ-quantile (0 ≤ φ ≤ 1) su dimensioni",
"documentation-stddev": "Calcola la deviazione standard della popolazione sulle dimensioni",
"documentation-stdvar": "Calcola la varianza standard della popolazione sulle dimensioni",
"documentation-sum": "Calcola la somma sulle dimensioni",
"documentation-topk": "Elementi k più grandi per valore campione"
},
"getFunctions": {
"documentation-abs": "Restituisce il vettore di input con tutti i valori di esempio convertiti nel loro valore assoluto.",
"documentation-absent": "Restituisce un vettore vuoto se il vettore passato ad esso ha elementi e un vettore a 1 elemento con il valore 1 se il vettore passato ad esso non ha elementi. Questo è utile per avvisare quando non esistono serie temporali per un dato nome di metrica e combinazione di etichette.",
"documentation-absent-over-time": "Restituisce un vettore vuoto se il vettore di intervallo passato ad esso ha elementi e un vettore a 1 elemento con il valore 1 se il vettore di intervallo passato ad esso non ha elementi.",
"documentation-avg-over-time": "Il valore medio di tutti i punti nell'intervallo specificato.",
"documentation-ceil": "Arrotonda i valori campione di tutti gli elementi in \"v\" al numero intero più vicino.",
"documentation-changes": "Per ogni serie temporale di input, \"changes(v range-vector)\" restituisce il numero di volte in cui il suo valore è cambiato all'interno dell'intervallo di tempo fornito come vettore istantaneo.",
"documentation-clamp": "Blocca i valori campione di tutti gli elementi in \"v\" per avere un limite inferiore di \"min\" e un limite superiore di \"max\".",
"documentation-clamp-max": "Blocca i valori campione di tutti gli elementi in \"v\" per avere un limite superiore di \"max\".",
"documentation-clamp-min": "Blocca i valori campione di tutti gli elementi in \"v\" per avere un limite inferiore di \"min\".",
"documentation-count-over-time": "Il conteggio di tutti i valori nell'intervallo specificato.",
"documentation-count-scalar": "Restituisce il numero di elementi in un vettore di serie temporali come scalare. Questo è in contrasto con l'operatore di aggregazione \"count()\", che restituisce sempre un vettore (uno vuoto se il vettore di input è vuoto) e consente il raggruppamento per etichette tramite una clausola \"by\".",
"documentation-day-of-month": "Restituisce il giorno del mese per ciascuno degli orari specificati in UTC. I valori restituiti sono compresi tra 1 e 31.",
"documentation-day-of-week": "Restituisce il giorno della settimana per ciascuno degli orari specificati in UTC. I valori restituiti sono compresi tra 0 e 6, dove 0 significa domenica, ecc.",
"documentation-day-of-year": "Restituisce il giorno dell'anno per ciascuno degli orari specificati in UTC. I valori restituiti sono compresi tra 1 e 365 per gli anni non bisestili e tra 1 e 366 per gli anni bisestili.",
"documentation-days-in-month": "Restituisce il numero di giorni del mese per ciascuno degli orari specificati in UTC. I valori restituiti sono compresi tra 28 e 31.",
"documentation-deg": "Converte i radianti in gradi per tutti gli elementi in v",
"documentation-delta": "Calcola la differenza tra il primo e l'ultimo valore di ciascun elemento della serie temporale in un vettore di intervallo \"v\", restituendo un vettore istantaneo con i delta dati e le etichette equivalenti. Il delta viene estrapolato per coprire l'intero intervallo di tempo come specificato nel selettore del vettore di intervallo, in modo che sia possibile ottenere un risultato non intero anche se i valori del campione sono tutti interi.",
"documentation-deriv": "Calcola la derivata al secondo della serie temporale in un vettore di intervallo \"v\", utilizzando la regressione lineare semplice.",
"documentation-double-exponential-smoothing": "Produce un valore livellato per le serie temporali in base all'intervallo in \"v\". Più basso è il fattore di livellamento \"sf\", maggiore è l'importanza data ai vecchi dati. Più alto è il fattore di tendenza \"tf\", più tendenze nei dati vengono considerate. Sia \"sf\" che \"tf\" devono essere compresi tra 0 e 1.",
"documentation-drop-common-labels": "Elimina tutte le etichette che hanno lo stesso nome e valore in tutte le serie nel vettore di input.",
"documentation-exp": "Calcola la funzione esponenziale per tutti gli elementi in \"v\".\nI casi speciali sono:\n* \"Exp(+Inf) = +Inf\" \n* \"Exp(NaN) = NaN\"",
"documentation-floor": "Arrotonda i valori campione di tutti gli elementi in \"v\" al numero intero più vicino.",
"documentation-histogram-avg": "Restituisce la media aritmetica dei valori osservati memorizzati in un istogramma nativo. I campioni che non sono istogrammi nativi vengono ignorati e non vengono visualizzati nel vettore restituito.",
"documentation-histogram-count": "Restituisce il conteggio delle osservazioni memorizzate in un istogramma nativo.",
"documentation-histogram-fraction": "Restituisce la frazione stimata di osservazioni tra i valori inferiore e superiore forniti.",
"documentation-histogram-quantile": "Calcola il φ-quantile (0 ≤ φ ≤ 1) dai bucket \"b\" di un istogramma. I campioni in 'b' sono i conteggi delle osservazioni in ciascun bucket. Ogni campione deve avere un'etichetta \"le\" in cui il valore dell'etichetta indica il limite superiore inclusivo del bucket (i campioni senza tale etichetta vengono ignorati silenziosamente.) Il tipo di metrica dell'istogramma fornisce automaticamente le serie temporali con il suffisso \"_bucket\" e le etichette appropriate.",
"documentation-histogram-stddev": "Restituisce la deviazione standard stimata delle osservazioni in un istogramma nativo, in base alla media geometrica dei bucket in cui si trovano le osservazioni.",
"documentation-histogram-stdvar": "Restituisce la varianza standard stimata delle osservazioni in un istogramma nativo.",
"documentation-histogram-sum": "Restituisce la somma delle osservazioni memorizzate in un istogramma nativo.",
"documentation-holt-winters": "Rinominato come double_exponential_smoothing in prometheus v3.x. Per le versioni di prometheus uguali e superiori a v3.0, utilizza double_exponential_smoothing. \n\nProduce un valore livellato per le serie temporali in base all'intervallo in \"v\". Più basso è il fattore di livellamento \"sf\", maggiore è l'importanza data ai vecchi dati. Più alto è il fattore di tendenza \"tf\", più tendenze nei dati vengono considerate. Sia \"sf\" che \"tf\" devono essere compresi tra 0 e 1.",
"documentation-hour": "Restituisce l'ora del giorno per ciascuno degli orari specificati in UTC. I valori restituiti sono compresi tra 0 e 23.",
"documentation-idelta": "Calcola la differenza tra gli ultimi due campioni nel vettore di intervallo \"v\", restituendo un vettore istantaneo con i delta dati e le etichette equivalenti.",
"documentation-increase": "Calcola l'incremento della serie temporale nel vettore di intervallo. Le interruzioni di monotonicità (come i reset del contatore dovuti ai riavvii del target) vengono regolate automaticamente. L'incremento viene estrapolato per coprire l'intero intervallo di tempo come specificato nel selettore del vettore di intervallo, in modo che sia possibile ottenere un risultato non intero anche se un contatore aumenta solo di incrementi interi.",
"documentation-info": "Restituisce i dettagli e i metadati più recenti su un gruppo di metriche, come le etichette e i valori correnti, senza eseguire alcun calcolo",
"documentation-irate": "Calcola il tasso di incremento istantaneo al secondo della serie temporale nel vettore di intervallo. Questo si basa sugli ultimi due punti dati. Le interruzioni di monotonicità (come i reset del contatore dovuti ai riavvii del target) vengono regolate automaticamente.",
"documentation-label-join": "Per ogni serie temporale in \"v\", unisce tutti i valori di tutte le \"src_labels\" utilizzando \"separator\" e restituisce la serie temporale con l'etichetta \"dst_label\" contenente il valore unito. In questa funzione può esserci un numero qualsiasi di \"src_labels\".",
"documentation-label-replace": "Per ogni serie temporale in \"v\", \"label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)\" corrisponde all'espressione regolare \"regex\" rispetto all'etichetta \"src_label\". Se corrisponde, la serie temporale viene restituita con l'etichetta \"dst_label\" sostituita dall'espansione di \"replacement\". \"$1\" viene sostituito con il primo sottogruppo corrispondente, \"$2\" con il secondo, ecc. Se l'espressione regolare non corrisponde, la serie temporale viene restituita invariata.",
"documentation-last-over-time": "Il valore del punto più recente nell'intervallo specificato.",
"documentation-ln": "Calcola il logaritmo naturale per tutti gli elementi in \"v\".\nI casi speciali sono:\n * \"ln(+Inf) = +Inf\"\n * \"ln(0) = -Inf\"\n * \"ln(x < 0) = NaN\"\n * \"ln(NaN) = NaN\"",
"documentation-log10": "Calcola il logaritmo decimale per tutti gli elementi in \"v\". I casi speciali sono equivalenti a quelli in \"ln\".",
"documentation-log2": "Calcola il logaritmo binario per tutti gli elementi in \"v\". I casi speciali sono equivalenti a quelli in \"ln\".",
"documentation-max-over-time": "Il valore massimo di tutti i punti nell'intervallo specificato.",
"documentation-min-over-time": "Il valore minimo di tutti i punti nell'intervallo specificato.",
"documentation-minute": "Restituisce il minuto dell'ora per ciascuno degli orari specificati in UTC. I valori restituiti sono compresi tra 0 e 59.",
"documentation-month": "Restituisce il mese dell'anno per ciascuno degli orari specificati in UTC. I valori restituiti sono compresi tra 1 e 12, dove 1 significa gennaio, ecc.",
"documentation-pi": "Restituisce pi",
"documentation-predict-linear": "Prevede il valore delle serie temporali \"t\" secondi da ora, in base al vettore di intervallo \"v\", utilizzando la regressione lineare semplice.",
"documentation-present-over-time": "Il valore 1 per qualsiasi serie nell'intervallo specificato.",
"documentation-quantile-over-time": "Il φ-quantile (0 ≤ φ ≤ 1) dei valori nell'intervallo specificato.",
"documentation-rad": "Converte i gradi in radianti per tutti gli elementi in v",
"documentation-rate": "Calcola il tasso medio di incremento al secondo della serie temporale nel vettore di intervallo. Le interruzioni di monotonicità (come i reset del contatore dovuti ai riavvii del target) vengono regolate automaticamente. Inoltre, il calcolo estrapola alle estremità dell'intervallo temporale, consentendo di perdere i graffi o l'allineamento imperfetto dei cicli di raschiatura con il periodo di tempo dell'intervallo.",
"documentation-resets": "Per ogni serie temporale di input, \"resets(v range-vector)\" restituisce il numero di reset del contatore all'interno dell'intervallo di tempo fornito come vettore istantaneo. Qualsiasi diminuzione del valore tra due campioni consecutivi viene interpretata come un reset del contatore.",
"documentation-round": "Arrotonda i valori campione di tutti gli elementi in \"v\" al numero intero più vicino. I pareggi vengono risolti arrotondando per eccesso. L'argomento facoltativo \"to_nearest\" consente di specificare il multiplo più vicino a cui devono essere arrotondati i valori campione. Questo multiplo può anche essere una frazione.",
"documentation-scalar": "Dato un vettore di input a elemento singolo, \"scalar(v instant-vector)\" restituisce il valore campione di quel singolo elemento come scalare. Se il vettore di input non ha esattamente un elemento, \"scalar\" restituirà \"NaN\".",
"documentation-sgn": "Restituisce un vettore con tutti i valori di esempio convertiti nel loro segno, definito come questo: 1 se v è positivo, -1 se v è negativo e 0 se v è uguale a zero.",
"documentation-sort": "Restituisce gli elementi del vettore ordinati in base ai loro valori campione, in ordine crescente.",
"documentation-sort-desc": "Restituisce gli elementi del vettore ordinati in base ai loro valori di esempio, in ordine decrescente.",
"documentation-sqrt": "Calcola la radice quadrata di tutti gli elementi in \"v\".",
"documentation-stddev-over-time": "La deviazione standard della popolazione dei valori nell'intervallo specificato.",
"documentation-stdvar-over-time": "La varianza standard della popolazione dei valori nell'intervallo specificato.",
"documentation-sum-over-time": "La somma di tutti i valori nell'intervallo specificato.",
"documentation-time": "Restituisce il numero di secondi trascorsi dal 1° gennaio 1970 UTC. Si noti che questo non restituisce effettivamente l'ora corrente, ma l'ora in cui l'espressione deve essere valutata.",
"documentation-timestamp": "Restituisce la marca temporale di ciascuno dei campioni del vettore specificato come numero di secondi dal 1° gennaio 1970 UTC.",
"documentation-vector": "Restituisce lo scalare \"s\" come vettore senza etichette.",
"documentation-year": "Restituisce l'anno per ciascuno degli orari specificati in UTC."
},
"getTrigonometricFunctions": {
"documentation-acos": "calcola l'arcocoseno di tutti gli elementi in {{argument}}",
"documentation-acosh": "calcola il coseno iperbolico inverso di tutti gli elementi in {{argument}}",
"documentation-asin": "calcola l'arcoseno di tutti gli elementi in {{argument}}",
"documentation-asinh": "calcola il seno iperbolico inverso di tutti gli elementi in {{argument}}",
"documentation-atan": "calcola l'arcotangente di tutti gli elementi in {{argument}}",
"documentation-atanh": "calcola la tangente iperbolica inversa di tutti gli elementi in {{argument}}",
"documentation-cos": "calcola il coseno di tutti gli elementi in {{argument}}",
"documentation-cosh": "calcola il coseno iperbolico di tutti gli elementi in {{argument}}",
"documentation-sin": "calcola il seno di tutti gli elementi in {{argument}}",
"documentation-sinh": "calcola il seno iperbolico di tutti gli elementi in {{argument}}",
"documentation-tan": "calcola la tangente di tutti gli elementi in {{argument}}",
"documentation-tanh": "calcola la tangente iperbolica di tutti gli elementi in {{argument}}"
}
},
"querybuilder": {
"feedback-link": {
"give-feedback": "Lascia un feedback",
"title-give-feedback": "L'esplora metriche è nuovo, facci sapere come possiamo migliorarlo"
},
"get-collapsed-info": {
"exemplars": "Esempi: {{value}}",
"format": "Formato: {{value}}",
"legend": "Legenda: {{value}}",
"step": "Incremento: {{value}}",
"type": "Tipo: {{value}}"
},
"get-placeholders": {
"browse": "Cerca metriche per nome",
"type": "Filtra per tipo"
},
"get-prom-types": {
"description-counter": "Una metrica cumulativa che rappresenta un singolo contatore monotonamente crescente il cui valore può solo aumentare o essere azzerato al riavvio.",
"description-gauge": "Una metrica che rappresenta un singolo valore numerico che può arbitrariamente salire e scendere.",
"description-histogram": "Un istogramma campiona le osservazioni (di solito cose come la durata delle richieste o le dimensioni delle risposte) e le conta in bucket configurabili.",
"description-native-histogram": "Gli istogrammi nativi sono diversi dagli istogrammi classici di Prometheus in diversi modi: i limiti dei bucket degli istogrammi nativi sono calcolati da una formula che dipende dalla scala (risoluzione) dell'istogramma nativo e non sono definiti dall'utente.",
"description-no-type": "Queste metriche non hanno un tipo definito nei metadati.",
"description-summary": "Un riepilogo campiona le osservazioni (di solito cose come la durata delle richieste e le dimensioni delle risposte) e può calcolare quantili configurabili su una finestra temporale scorrevole.",
"description-unknown": "A queste metriche è stato assegnato il tipo sconosciuto nei metadati.",
"label-counter": "Contatore",
"label-gauge": "Calibro",
"label-histogram": "Istogramma",
"label-native-histogram": "Istogramma nativo",
"label-no-type": "Nessun tipo",
"label-summary": "Riepilogo",
"label-unknown": "Sconosciuto"
},
"handle-function": {
"text": {
"query-parsing-is-ambiguous": "L'analisi della query è ambigua."
}
},
"label-filter-item": {
"aria-label-remove": "Rimuovi {{name}}",
"placeholder-select-label": "Seleziona etichetta",
"placeholder-select-value": "Seleziona valore"
},
"label-filters": {
"label-filters": "Filtri etichetta",
"label-label-filters": "Filtri etichetta",
"tooltip-label-filters": "Facoltativo: utilizzato per filtrare la metrica selezionata per questo tipo di query."
},
"label-param-editor": {
"loadingMessage-loading-labels": "Caricamento delle etichette",
"noOptionsMessage-no-labels-found": "Nessuna etichetta trovata"
},
"metric-combobox": {
"async-select": {
"aria-label-open-metrics-explorer": "Apri Esplora metriche",
"placeholder-select-metric": "Seleziona metrica",
"tooltip-open-metrics-explorer": "Apri Esplora metriche"
},
"label-metric": "Metrica",
"tooltip-metric": "Facoltativo: restituisce un elenco di valori di etichetta per il nome dell'etichetta nella metrica specificata."
},
"metrics-modal": {
"aria-label-browse-metrics": "Sfoglia metriche",
"currently-selected": "Attualmente selezionata: {{selected}}",
"metrics-pre-filtered": "Queste metriche sono state pre-filtrate in base alle etichette scelte nei filtri delle etichette.",
"title-metrics-explorer": "Esplora metriche"
},
"nested-query": {
"label": {
"ignoring": "Ignora",
"on": "In data"
},
"operator": "Operatore",
"tooltip-remove-match": "Rimuovi corrispondenza",
"vector-matches": "Corrispondenze vettoriali"
},
"operation-editor": {
"not-found": "{{id}} operazione non trovata",
"title-remove": "Rimuovi {{name}}"
},
"operation-header": {
"placeholder-replace-with": "Sostituisci con",
"title-click-to-view-alternative-operations": "Fai clic per visualizzare operazioni alternative",
"title-remove-operation": "Rimuovi operazione"
},
"operation-info-button": {
"title-click-to-show-description": "Fai clic per mostrare la descrizione",
"title-remove-operation": "Rimuovi operazione"
},
"operation-list": {
"operations": "Operazioni",
"placeholder-search": "Cerca",
"title-add-operation": "Aggiungi operazione"
},
"operation-param-editor": {
"title-add": "Aggiungi {{name}}",
"title-remove": "Rimuovi {{name}}"
},
"operation-utils": {
"getAggregationExplainer": {
"label-by": "Calcola {{aggregationName}} sulle dimensioni preservando {{labelWord}} {{labels}}.",
"label-default": "Calcola {{aggregationName}} sulle dimensioni.",
"label-without": "Calcola {{aggregationName}} sulle dimensioni {{labels}}. Tutte le altre etichette vengono conservate."
}
},
"prom-query-builder-options": {
"aria-label-exemplars": "Commutatori esemplari.",
"aria-label-format": "Formatta casella combinata",
"aria-label-lower-limit-parameter": "Casella di testo passaggio minimo, imposta il limite inferiore per il parametro passaggio",
"aria-label-select-resolution": "Seleziona risoluzione",
"aria-label-type": "Gruppo pulsanti di opzione di digitazione",
"format-options": {
"label-heatmap": "Mappa di calore",
"label-table": "Tabella",
"label-time-series": "Serie temporale"
},
"label-exemplars": "Esempi",
"label-format": "Formato",
"label-min-step": "Passaggio minimo",
"label-resolution": "Risoluzione",
"label-type": "Tipo",
"placeholder-auto": "automatico",
"title-options": "Opzioni",
"tooltip-min-step": "Un ulteriore limite inferiore per il parametro passaggio della query Prometheus e per le variabili <2>{{interval}}</2> e <4>{{rateInterval}}</4>."
},
"prom-query-editor-selector": {
"body-syntax-error": "Si è verificato un errore di sintassi oppure la struttura della query non può essere visualizzata quando si passa alla modalità generatore. Parti della query potrebbero andare perse.",
"confirmText-continue": "Continua",
"kick-start-your-query": "Avvia la query",
"label-explain": "Spiega",
"run-queries": "Esegui query",
"title-parsing-error-switch-builder": "Errore di analisi: passare alla modalità generatore?"
},
"prom-query-legend-editor": {
"aria-label-legend": "Casella combinata legenda",
"label-legend": "Legenda",
"placeholder-select-legend-mode": "Seleziona la modalità legenda",
"tooltip-legend": "Sovrascrittura o modello del nome della serie. Es. {{templateExample}} verrà sostituito con il valore dell'etichetta per {{labelName}}."
},
"query-builder-hints": {
"hint-details": "suggerimento: {{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "Builder",
"label-code": "Codice"
}
},
"query-pattern": {
"apply-query": "Applica query",
"aria-label-apply-query-starter-button": "pulsante di avvio applica query",
"aria-label-back-button": "pulsante indietro",
"aria-label-create-new-query-button": "pulsante crea nuova query",
"aria-label-raw-query": "query non elaborata {{patternName}}",
"aria-label-use-this-query-button": "utilizza questo pulsante di query",
"back": "Indietro",
"create-new-query": "Crea nuova query",
"use-this-query": "Utilizza questa query"
},
"query-patterns-modal": {
"aria-label-close-kick-start-your-query-modal": "chiudi avvia modalità query",
"aria-label-kick-start-your-query-modal": "Avvia il modale della query",
"aria-label-toggle-query-starter": "apri e chiudi la scheda di avvio della query {{patternType}}",
"close": "Chiudi",
"description-kick-start-your-query": "Avvia la query selezionando una di queste query. Ora puoi continuare a completare la query.",
"label-toggle-query-starter": "Avvii query {{patternType}}",
"title-kick-start-your-query": "Avvia la query"
},
"raw-query": {
"aria-label-selector": "selettore"
},
"results-table": {
"content-descriptive-type": "Quando si crea un {{descriptiveType}}, Prometheus mostra più serie con il contatore di tipo. ",
"description": "Descrizione",
"message-expand-label-filters": "Non sono state trovate metriche. Prova ad espandere i filtri delle etichette.",
"message-expand-search": "Non sono state trovate metriche. Prova ad espandere la ricerca e i filtri.",
"message-no-metrics-found": "Non sono state trovate metriche nell'origine dati.",
"name": "Nome",
"type": "Tipo"
}
}
}
}

View File

@@ -1,492 +0,0 @@
{
"grafana-prometheus": {
"components": {
"annotation-query-editor": {
"annotation-data-load-error": "注釈データの読み込みエラー!",
"aria-label-lower-limit-parameter": "ステップパラメーターの下限を設定",
"label-min-step": "最小ステップ",
"label-series-value-as-timestamp": "タイムスタンプとしての系列値",
"label-tags": "タグ",
"label-text": "テキスト",
"label-title": "タイトル",
"placeholder-auto": "自動",
"tooltip-either-pattern-example-instance-replaced-label": "名前またはパターンのいずれかを使用します。例えば、{{labelTemplate}}はラベル{{labelName}}のラベル値に置き換えられます。",
"tooltip-min-step": "Prometheusクエリのステップパラメーターと<2>{{intervalVar}}</2>および<4>{{rateIntervalVar}}</4>変数の追加下限。",
"tooltip-timestamp-milliseconds-series-value-seconds-multiply": "タイムスタンプの単位はミリ秒です。系列値の単位が秒の場合、その範囲ベクトルに1000を掛けます。"
},
"get-query-type-options": {
"description": {
"instant-query-range": "インスタントクエリと範囲クエリを実行"
},
"label": {
"both": "両方"
},
"range-options": {
"description": {
"query-range": "時間範囲にわたってクエリを実行"
},
"label": {
"instant": "インスタント",
"range": "範囲"
}
}
},
"label-selector": {
"aria-label-filter-expression-for-label": "ラベルのフィルター式",
"description-select-labels": "ラベル値を選択すると、可能なラベルの組み合わせのみが表示されます。",
"select-labels-to-search-in": "2. 検索するラベルを選択"
},
"metric-selector": {
"aria-label-filter-expression-for-metric": "メトリックのフィルター式",
"aria-label-limit-results-from-series-endpoint": "系列エンドポイントからの結果を制限",
"description-series-limit": "制限はすべてのメトリック、ラベル、値に適用されます。デフォルトの制限を使用するには、フィールドを空のままにしてください。制限を無効にしてすべてを取得するには0に設定します。これによりパフォーマンスの問題が発生する可能性があります。",
"label-select-metric": "メトリックを選択すると、可能なラベルのみが表示されます。ラベルには以下の系列制限が適用されます。",
"select-a-metric": "メトリックを選択",
"series-limit": "系列制限"
},
"prom-cheat-sheet": {
"prom-ql-cheat-sheet": "PromQLチートシート"
},
"prom-exemplar-field": {
"exemplars": "エグゼンプラー",
"tooltip-disable-query": "エグゼンプラーを使用したクエリを無効化",
"tooltip-enable-query": "エグゼンプラーを使用したクエリを有効化"
},
"prom-explore-extra-field": {
"aria-label-prometheus-extra-field": "Prometheus追加フィールド",
"aria-label-query-type-field": "クエリタイプフィールド",
"aria-label-step-field": "ステップフィールド",
"min-step": "最小ステップ",
"query-type": "クエリタイプ",
"tooltip-units-builtin-variables-example-interval-rateinterval": "ここでは時間単位と組み込み変数を使用できます。例:{{example1}}、{{example2}}、{{example3}}、{{example4}}、{{example5}}、{{example6}}、{{example7}}(単位が指定されていない場合のデフォルト:{{default}}"
},
"prom-query-field": {
"placeholder-enter-a-prom-ql-query": "PromQLクエリを入力…"
},
"prom-variable-query-editor": {
"aria-label-classic-query": "クラシッククエリ",
"aria-label-metric-regex": "メトリック正規表現",
"aria-label-metric-selector": "メトリックセレクター",
"aria-label-prometheus-query": "Prometheusクエリ",
"aria-label-query-type": "クエリタイプ",
"aria-label-series-query": "系列クエリ",
"label-classic-query": "クラシッククエリ",
"label-label": "ラベル",
"label-metric-regex": "メトリック正規表現",
"label-query": "クエリ",
"label-query-type": "クエリタイプ",
"label-series-query": "系列クエリ",
"placeholder-classic-query": "クラシッククエリ",
"placeholder-metric-regex": "メトリック正規表現",
"placeholder-prometheus-query": "Prometheusクエリ",
"placeholder-select-query-type": "クエリタイプを選択",
"placeholder-series-query": "系列クエリ",
"returns-metrics-matching-specified-metric-regex": "指定された正規表現に一致するメトリックのリストを返します。",
"tooltip-classic-query": "Prometheus変数クエリエディターの元の実装。これらのドキュメントに記載されているように、正しいクエリタイプとパラメーターを含む文字列を入力してください。例{{exampleQuery}}。",
"tooltip-label": "メトリックが指定されていない限り、すべてのメトリックのラベル名に対するラベル値のリストを返します。",
"tooltip-metric-regex": "ラベル名のリストを返します。オプションで、指定されたメトリック正規表現でフィルタリングすることも可能です。",
"tooltip-query": "クエリのPrometheusクエリ結果のリストを返します。これには、Prometheus関数を含めることもできます。例: {{exampleQuery}}。",
"tooltip-query-type": "Prometheusデータソースプラグインは、テンプレート変数に以下のクエリタイプを入力します。",
"tooltip-series-query": "ラベル付きメトリック、メトリックのみ、またはラベルのみを入力します。例: {{example1}}、{{example2}}、または{{example3}}。入力されたデータに関連する時系列のリストを返します。"
},
"selector-actions": {
"aria-label-selector": "セレクター",
"aria-label-selector-clear-button": "セレクタークリアボタン",
"aria-label-use-selector-as-metrics-button": "メトリックボタンにセレクターを使用",
"aria-label-use-selector-for-query-button": "クエリボタンにセレクターを使用",
"aria-label-validate-submit-button": "送信ボタンを検証",
"clear": "消去",
"resulting-selector": "4. 結果セレクター",
"use-as-rate-query": "レートクエリとして使用",
"use-query": "このクエリを使用",
"validate-selector": "セレクターを検証"
},
"value-selector": {
"aria-label-filter-expression-for-label-values": "ラベル値のフィルター式",
"aria-label-values-for": "{{labelKey}}の値",
"description-search-field-values-across-selected-labels": "検索フィールドを使用して選択したラベル全体の値を検索します。",
"select-multiple-values-for-your-labels": "3. ラベルの値を(複数)選択"
}
},
"configuration": {
"alerting-settings-overhaul": {
"label-allow-as-recording-rules-target": "記録ルールのターゲットとして許可",
"label-manage-alerts-via-alerting-ui": "アラートUIでアラートを管理",
"title-alerting": "アラート",
"tooltip-allow-as-recording-rules-target": "このデータソースを記録ルール書き込みのターゲットとして選択できるようにします。",
"tooltip-manage-alerts-via-alerting-ui": "このデータソースのアラートルールを管理します。他のアラートリソースを管理するには、Alertmanagerデータソースを追加します。"
},
"config-editor": {
"browser-access-mode-error": "Prometheusデータソースのブラウザアクセスモードは利用できなくなりました。サーバーアクセスモードに切り替えてください。",
"description-advanced-settings": "追加設定は、データソースをより詳細に制御するために指定できるオプション設定です。",
"title-advanced-settings": "詳細設定",
"title-error": "エラー"
},
"data-source-http-settings-overhaul": {
"tooltip-browser-access-mode": "アクセス方法は<1>ブラウザ</1>です。つまり、URLにブラウザからアクセスできる必要があります。",
"tooltip-http-url": "完全なHTTP URLを指定してください例: {{exampleURL}}",
"tooltip-server-access-mode": "アクセス方法は<1>サーバー</1>です。つまり、URLにGrafanaバックエンド/サーバーからアクセスできる必要があります。"
},
"docs-tip": {
"visit-docs-for-more-details-here": "詳細についてはこちらのドキュメントをご覧ください。"
},
"exemplar-setting": {
"label-data-source": "データソース",
"label-internal-link": "内部リンク",
"label-label-name": "ラベル名",
"label-remove-exemplar-link": "エグゼンプラーリンクを削除",
"label-url": "URL",
"label-url-label": "URLラベル",
"placeholder-go-to-examplecom": "example.comに移動",
"placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}",
"placeholder-trace-id": "traceID",
"title-remove-exemplar-link": "エグゼンプラーリンクを削除",
"tooltip-data-source": "エグゼンプラーによる移動先のデータソース。",
"tooltip-internal-link": "内部リンクがある場合は、このオプションを有効にしてください。有効にすると、データソースセレクターが表示されます。エグゼンプラーデータのバックエンドトレーシングデータストアを選択してください。",
"tooltip-label-name": "traceIDを取得するために使用されるラベルオブジェクト内のフィールド名。",
"tooltip-url": "ユーザーがトレースを確認するためにアクセスするトレースバックエンドのURL",
"tooltip-url-label": "エグゼンプラーのtraceIDフィールドのボタンラベルを上書きするために使用します。"
},
"exemplars-settings": {
"add": "追加",
"no-exemplars-configurations": "エグゼンプラー設定がありません",
"title-exemplars": "エグゼンプラー"
},
"prom-settings": {
"aria-label-default-editor": "デフォルトエディター(コードまたはビルダー)",
"aria-label-prom-type-type": "{{promType}}タイプ",
"aria-label-prometheus-type": "Prometheusタイプ",
"aria-label-select-http-method": "HTTPメソッドを選択",
"editor-options": {
"label-builder": "ビルダー",
"label-code": "コード"
},
"label-cache-level": "キャッシュレベル",
"label-custom-query-parameters": "カスタムクエリパラメーター",
"label-default-editor": "デフォルトエディター",
"label-disable-metrics-lookup": "メトリック検索を無効化",
"label-disable-recording-rules-beta": "記録ルールを無効化(ベータ版)",
"label-http-method": "HTTPメソッド",
"label-incremental-querying-beta": "増分クエリ(ベータ版)",
"label-prom-type-version": "{{promType}}バージョン",
"label-prometheus-type": "Prometheusタイプ",
"label-query-overlap-window": "クエリ重複ウィンドウ",
"label-query-timeout": "クエリタイムアウト",
"label-scrape-interval": "スクレイプ間隔",
"label-series-limit": "系列制限",
"label-use-series-endpoint": "系列エンドポイントを使用",
"more-info": "データソースでのPrometheusタイプとバージョンの設定の詳細については、<2>プロビジョニングドキュメント</2>をご覧ください。",
"placeholder-example-maxsourceresolutionmtimeout": "例:{{example}}",
"title-interval-behaviour": "間隔の動作",
"title-other": "その他",
"title-performance": "パフォーマンス",
"title-query-editor": "クエリエディター",
"tooltip-cache-level": "エディタークエリのブラウザーキャッシュレベルを設定します。高カーディナリティデータソースには、より高いキャッシュ設定を推奨します。",
"tooltip-custom-query-parameters": "PrometheusクエリURLにカスタムパラメーターを追加します。例: {{example1}}、{{example2}}、{{example3}}、または{{example4}}。複数のパラメーターは{{concatenationChar}}で連結する必要があります。",
"tooltip-default-editor": "このデータソースのすべてのユーザーにデフォルトエディターオプションを設定します。",
"tooltip-disable-metrics-lookup": "このオプションをオンにすると、クエリフィールドのオートコンプリートでメトリック選択とメトリック/ラベルサポートが無効になります。これは、大規模なPrometheusインスタンスをご利用でパフォーマンスの問題がある場合に役立ちます。",
"tooltip-disable-recording-rules-beta": "この機能は記録ルールを無効にします。ダッシュボードのパフォーマンスを向上させるには、この機能をオンにしてください",
"tooltip-http-method": "PrometheusデータソースをクエリするにはPOSTまたはGET HTTPメソッドのいずれかを使用できます。POSTメソッドだと、より大きなクエリを実行できるため、推奨される方法です。Prometheusのバージョンが2.1より古い場合、またはネットワークでPOSTリクエストが制限されている場合は、これをGETに変更してください。",
"tooltip-incremental-querying-beta": "この機能は相対クエリのデフォルト動作が変更され、常にPrometheusインスタンスから新しいデータをリクエストする代わりに、クエリ結果をキャッシュして新しいレコードのみをリクエストするようになります。データベースとネットワークの負荷を軽減するには、これをオンにしてください。",
"tooltip-prom-type-version": "自動設定されていない場合、これを使用して{{promType}}インスタンスのバージョンを設定してください。",
"tooltip-prometheus-type": "Prometheus、Cortex、Mimir、Thanosなど、Prometheusデータベースのタイプに設定してください。このフィールドを変更すると、現在の設定が保存されます。一部のPrometheusのタイプでは、さまざまなAPIがサポートされているか、またはサポートされていない場合があります。例えば、一部のタイプではパフォーマンス向上のためにラベルクエリの正規表現マッチングをサポートしています。また、タイプによってはメタデータ用のAPIがあります。これを正しく設定しないと、メトリックとラベルのクエリ時に異常な動作が発生する可能性があります。正しいタイプを入力するために、Prometheusのドキュメントを確認してください。",
"tooltip-query-overlap-window": "{{example1}}や{{example2}}または{{example3}}のような期間を設定してください。デフォルトは{{default}}です。この期間は、各増分リクエストの期間に追加されます。",
"tooltip-query-timeout": "Prometheusクエリタイムアウトを設定します。",
"tooltip-scrape-interval": "この間隔は、Prometheusがターゲットをスクレイプする頻度です。Prometheus設定ファイルで指定された一般的なスクレイプと評価間隔に設定してください。これをPrometheus設定ファイルの間隔より大きな値に設定すると、Grafanaはこの間隔に従ってデータを評価するため、データポイントが少なくなります。デフォルトは{{default}}です。",
"tooltip-series-limit": "制限は両方のエンドポイント系列とラベルのすべてのリソースメトリック、ラベル、値に適用されます。デフォルトの制限40000を使用するには、フィールドを空のままにしてください。制限を無効にしてすべてを取得するには0に設定します。これによりパフォーマンスの問題が発生する可能性があります。デフォルトの制限は40000です。",
"tooltip-use-series-endpoint": "このオプションをオンにすると、{{exampleParameter}}パラメーターを持つラベル値エンドポイントより、{{exampleParameter}}パラメーターを持つ系列エンドポイントが優先されます。ラベル値エンドポイントの方がパフォーマンスが高いと考えられていますが、ラベル値エンドポイントにはGETメソッドしかないのに対し、系列にはPOSTメソッドがあるため、系列を好むユーザーもいます。"
}
},
"metrics-browser": {
"disabled-label": "(無効化)",
"enabled-label": "メトリックブラウザ"
},
"prom-query-legend-editor": {
"get-legend-mode-options": {
"description-auto": "一意のラベルのみを含む",
"description-custom": "命名テンプレートを提供する",
"description-verbose": "すべてのラベル名と値",
"label-auto": "自動",
"label-custom": "カスタム",
"label-verbose": "詳細"
}
},
"promql": {
"getAggregationOptions": {
"documentation-avg": "次元間の平均を計算",
"documentation-bottomk": "サンプル値が最小のk個の要素",
"documentation-count": "ベクトル内の要素数をカウント",
"documentation-count-values": "同じ値を持つ要素の数をカウントする",
"documentation-group": "結果のベクトルのすべての値は1です",
"documentation-max": "次元での最大値を選択",
"documentation-min": "次元での最小値を選択",
"documentation-quantile": "次元間でφ分位数 (0 ≤ φ ≤ 1) を計算します",
"documentation-stddev": "次元にわたる母集団標準偏差を計算",
"documentation-stdvar": "次元にわたる母集団標準分散を計算",
"documentation-sum": "次元の合計を計算",
"documentation-topk": "サンプル値が最大のk個の要素"
},
"getFunctions": {
"documentation-abs": "すべてのサンプル値が絶対値に変換された入力ベクトルを返します。",
"documentation-absent": "渡されたベクトルに要素がある場合は空のベクトルを返し、渡されたベクトルに要素がない場合は値1の1要素ベクトルを返します。これは、指定されたメトリック名とラベルの組み合わせに時系列が存在しない場合に警告するのに役立ちます。",
"documentation-absent-over-time": "渡された範囲ベクトルに要素がある場合は空のベクトルを返し、渡された範囲ベクトルに要素がない場合は値1の1要素ベクトルを返します。",
"documentation-avg-over-time": "指定された間隔内のすべてのポイントの平均値。",
"documentation-ceil": "「v」のすべての要素のサンプル値を最も近い整数に切り上げます。",
"documentation-changes": "入力時系列ごとに、'changes(v range-vector)'は、指定された時間範囲内でその値が変更された回数をインスタントベクトルとして返します。",
"documentation-clamp": "「v」のすべての要素のサンプル値を「min」の下限と「max」の上限に固定します。",
"documentation-clamp-max": "「v」のすべての要素のサンプル値を「max」の上限に固定します。",
"documentation-clamp-min": "「v」のすべての要素のサンプル値を「min」の下限に固定します。",
"documentation-count-over-time": "指定された間隔のすべての値の個数。",
"documentation-count-scalar": "時系列ベクトルの要素数をスカラーとして返します。これは、常にベクトル (入力ベクトルが空の場合は空のベクトル) を返し、「by」句を介してラベルでグループ化できる「count()」集計演算子とは対照的です。",
"documentation-day-of-month": "UTCで指定された各時間の月の日を返します。返される値は1から31です。",
"documentation-day-of-week": "UTCで指定された各時間の曜日を返します。返される値は0から6までで、0は日曜日などを意味します。",
"documentation-day-of-year": "指定された各時刻の1年の日をUTCで返します。返される値は、非うるう年では1から365、うるう年では1から366です。",
"documentation-days-in-month": "UTCで指定された時間ごとに、その月の日数を返します。返される値は28から31です。",
"documentation-deg": "vのすべての要素のラジアンを度に変換します",
"documentation-delta": "範囲ベクトル「v」の各時系列要素の最初と最後の値の差を計算し、指定されたデルタと同等のラベルを持つインスタントベクトルを返します。デルタは、範囲ベクトルセレクターで指定されたフルタイム範囲をカバーするように外挿されるため、サンプル値がすべて整数であっても、非整数の結果を得ることができます。",
"documentation-deriv": "単純な線形回帰を使用して、範囲ベクトル「v」の時系列の秒単位の導関数を計算します。",
"documentation-double-exponential-smoothing": "「v」の範囲に基づいて、時系列の平滑化された値を生成します。平滑化係数「sf」が小さいほど、古いデータが重要になります。トレンド係数「tf」が高いほど、データのトレンドが考慮されます。「sf」と「tf」の両方が0と1の間でなければなりません。",
"documentation-drop-common-labels": "入力ベクトルのすべての系列にわたって、同じ名前と値を持つすべてのラベルをドロップします。",
"documentation-exp": "「v」のすべての要素の指数関数を計算します。\n特殊なケースは次のとおりです。\n* 'Exp(+Inf) = +Inf' \n* 'Exp(NaN) = NaN'",
"documentation-floor": "「v」のすべての要素のサンプル値を最も近い整数に切り下げます。",
"documentation-histogram-avg": "ネイティブヒストグラムに保存されている観測値の算術平均を返します。ネイティブヒストグラムではないサンプルは無視され、返されるベクトルには表示されません。",
"documentation-histogram-count": "ネイティブヒストグラムに保存されている観測値の数を返します。",
"documentation-histogram-fraction": "指定された下限値と上限値の間の観測値の推定割合を返します。",
"documentation-histogram-quantile": "ヒストグラムのバケット「b」からφ分位数 (0 ≤ φ ≤ 1) を計算します。「b」のサンプルは、各バケットの観測値の数です。各サンプルにはラベル「le」が必要です。ラベル値は、バケットの上限を含むことを示します。(そのようなラベルのないサンプルは、黙って無視されます。) ヒストグラムメトリックタイプは、自動的に「_bucket」サフィックスと適切なラベルを持つ時系列を提供します。",
"documentation-histogram-stddev": "観測値が存在するバケットの幾何平均に基づいて、ネイティブヒストグラムの観測値の推定標準偏差を返します。",
"documentation-histogram-stdvar": "ネイティブヒストグラムの観測値の推定標準分散を返します。",
"documentation-histogram-sum": "ネイティブヒストグラムに保存されている観測値の数を返します。",
"documentation-holt-winters": "Prometheus v3.xでdouble_exponential_smoothingに名前が変更されました。v3.0以降のPrometheusバージョンでは、double_exponential_smoothingを使用してください。「v」の範囲に基づいて、時系列の平滑化された値を生成します。平滑化係数「sf」が小さいほど、古いデータが重要になります。トレンド係数「tf」が高いほど、データのトレンドが考慮されます。「sf」と「tf」の両方が0と1の間でなければなりません。",
"documentation-hour": "UTCで指定された各時刻の時間を返します。返される値は0から23です。",
"documentation-idelta": "範囲ベクトル「v」の最後の2つのサンプルの差を計算し、指定されたデルタと同等のラベルを持つインスタントベクトルを返します。",
"documentation-increase": "範囲ベクトルの時系列の増加を計算します。単調性の破れ (ターゲットの再起動によるカウンターのリセットなど) は自動的に調整されます。増加は、範囲ベクトルセレクターで指定された全時間範囲をカバーするように外挿されるため、カウンターが整数だけ増加する場合でも、非整数の結果を得ることができます。",
"documentation-info": "計算を行わずに、ラベルや現在の値など、メトリックのグループに関する最新の詳細とメタデータを返します",
"documentation-irate": "範囲ベクトル内の時系列の1秒あたりの瞬間的な増加率を計算します。これは、最後の2つのデータポイントに基づいています。単調性の破れ (ターゲットの再起動によるカウンターのリセットなど) は自動的に調整されます。",
"documentation-label-join": "「v」の各時系列について、「separator」を使用してすべての「src_labels」のすべての値を結合し、結合された値を含むラベル「dst_label」を持つ時系列を返します。この関数には任意の数の「src_labels」を含めることができます。",
"documentation-label-replace": "「v」の各時系列について、「label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)」は、ラベル「src_label」に対して正規表現「regex」と一致します。一致する場合、時系列はラベル「dst_label」を「replacement」の拡張に置き換えて返されます。「$1」は最初の一致するサブグループに置き換えられ、「$2」は2番目などに置き換えられます。正規表現が一致しない場合、時系列は変更されずに返されます。",
"documentation-last-over-time": "指定された間隔の最新のポイント値。",
"documentation-ln": "「v」のすべての要素の自然対数を計算します。\n特殊なケースは次のとおりです。\n* 'ln(+Inf) = +Inf'\n * 'ln(0) = -Inf'\n * 'ln(x < 0) = NaN'\n * 'ln(NaN) = NaN'",
"documentation-log10": "「v」のすべての要素の10進対数を計算します。特殊なケースは「ln」のケースと同等です。",
"documentation-log2": "「v」のすべての要素の二進対数を計算します。特殊なケースは「ln」のケースと同等です。",
"documentation-max-over-time": "指定された間隔内のすべてのポイントの最大値。",
"documentation-min-over-time": "指定された間隔内のすべてのポイントの最小値。",
"documentation-minute": "UTCで指定された各時間の分を返します。返される値は0から59です。",
"documentation-month": "UTCで指定された各時間の年の月を返します。返される値は1から12までで、1は1月などを意味します。",
"documentation-pi": "π (パイ) を返します",
"documentation-predict-linear": "単純な線形回帰を使用して、範囲ベクトル「v」に基づいて、今から「t」秒後の時系列の値を予測します。",
"documentation-present-over-time": "指定された間隔内の任意の系列の値1。",
"documentation-quantile-over-time": "指定された間隔の値のφ分位数 (0 ≤ φ ≤ 1)。",
"documentation-rad": "vのすべての要素の度数をラジアンに変換します",
"documentation-rate": "範囲ベクトル内の時系列について、秒あたりの平均増加率を計算します。単調性の破れ (ターゲットの再起動によるカウンターのリセットなど) は自動的に調整されます。また、計算では時間範囲の端まで外挿を行い、スクレイプの見逃しや、スクレイプサイクルと範囲の期間との不完全なアライメントに対応します。",
"documentation-resets": "入力時系列ごとに、'resets(v range-vector)'は、指定された時間範囲内のカウンターリセットの数をインスタントベクトルとして返します。2つの連続するサンプル間の値の減少は、カウンターリセットとして解釈されます。",
"documentation-round": "「v」のすべての要素のサンプル値を最も近い整数に丸めます。端数は切り上げられます。オプションの「to_nearest」引数を使用すると、サンプル値を丸める最も近い倍数を指定できます。この倍数は分数でもかまいません。",
"documentation-scalar": "単一要素の入力ベクトルを指定すると、'scalar(v instant-vector)'はその単一要素のサンプル値をスカラーとして返します。要素がちょうど1つでない場合、'scalar'は'NaN'を返します。",
"documentation-sgn": "すべてのサンプル値が符号に変換されたベクトルを返します。これは次のように定義されます。vが正の場合は1、vが負の場合は-1、vがゼロの場合は0。",
"documentation-sort": "サンプル値でソートされたベクトル要素を昇順で返します。",
"documentation-sort-desc": "サンプル値で並べ替えたベクトル要素を降順で返します。",
"documentation-sqrt": "「v」のすべての要素の平方根を計算します。",
"documentation-stddev-over-time": "指定された間隔の値の母集団標準分散。",
"documentation-stdvar-over-time": "指定された間隔の値の母集団標準分散。",
"documentation-sum-over-time": "指定された間隔のすべての値の合計。",
"documentation-time": "1970年1月1日 (UTC) 以降の秒数を返します。これは実際には現在の時刻を返すのではなく、式が評価される時刻を返すことに注意してください。",
"documentation-timestamp": "指定されたベクトルの各サンプルのタイムスタンプを、1970年1月1日 (UTC) からの秒数として返します。",
"documentation-vector": "スカラー値sをラベルのないベクトルとして返します。",
"documentation-year": "UTCで指定された各時間の年を返します。"
},
"getTrigonometricFunctions": {
"documentation-acos": "{{argument}}のすべての要素のアークコサインを計算します",
"documentation-acosh": "{{argument}}のすべての要素の逆双曲線コサインを計算します",
"documentation-asin": "{{argument}}のすべての要素のアークサインを計算します",
"documentation-asinh": "{{argument}}のすべての要素の逆双曲線サインを計算します",
"documentation-atan": "{{argument}}のすべての要素のアークタンジェントを計算します",
"documentation-atanh": "{{argument}}のすべての要素の逆双曲線タンジェントを計算します",
"documentation-cos": "{{argument}}のすべての要素のコサインを計算します",
"documentation-cosh": "{{argument}}のすべての要素の双曲線コサインを計算します",
"documentation-sin": "{{argument}}のすべての要素のサインを計算します",
"documentation-sinh": "{{argument}}のすべての要素の双曲線サインを計算します",
"documentation-tan": "{{argument}}のすべての要素のタンジェントを計算します",
"documentation-tanh": "{{argument}}のすべての要素の双曲線タンジェントを計算します"
}
},
"querybuilder": {
"feedback-link": {
"give-feedback": "フィードバックを送信",
"title-give-feedback": "メトリックエクスプローラーは新機能です。改善点についてお聞かせください"
},
"get-collapsed-info": {
"exemplars": "例示データ:{{value}}",
"format": "フォーマット:{{value}}",
"legend": "凡例:{{value}}",
"step": "ステップ値:{{value}}",
"type": "タイプ:{{value}}"
},
"get-placeholders": {
"browse": "名前でメトリックを検索",
"type": "タイプで絞り込む"
},
"get-prom-types": {
"description-counter": "単一の単調増加カウンターを表す累積メトリックです。その値は増加するか、再起動時にゼロにリセットされるかのみです。",
"description-gauge": "任意に増減する単一の数値を表すメトリック。",
"description-histogram": "ヒストグラムは、観測値 (通常はリクエスト期間や応答サイズなど) をサンプリングし、設定可能なバケットでカウントします。",
"description-native-histogram": "ネイティブヒストグラムは、従来のPrometheusヒストグラムとは多くの点で異なります。ネイティブヒストグラムのバケット境界は、ネイティブヒストグラムのスケール (分解能) に依存する式によって計算され、ユーザー定義ではありません。",
"description-no-type": "これらのメトリックには、メタデータで定義されたタイプがありません。",
"description-summary": "要約は観測値 (通常はリクエスト期間や応答サイズなど) をサンプリングし、スライディング時間ウィンドウにわたって構成可能な分位数を計算できます。",
"description-unknown": "これらのメトリックには、メタデータでunknownタイプが指定されています。",
"label-counter": "カウンター",
"label-gauge": "ゲージ",
"label-histogram": "ヒストグラム",
"label-native-histogram": "ネイティブヒストグラム",
"label-no-type": "型なし",
"label-summary": "概要",
"label-unknown": "不明"
},
"handle-function": {
"text": {
"query-parsing-is-ambiguous": "クエリ解析が曖昧です。"
}
},
"label-filter-item": {
"aria-label-remove": "{{name}}を削除",
"placeholder-select-label": "ラベルを選択",
"placeholder-select-value": "値を選択"
},
"label-filters": {
"label-filters": "ラベルフィルター",
"label-label-filters": "ラベルフィルター",
"tooltip-label-filters": "オプション:このクエリタイプのメトリック選択をフィルタリングするために使用します。"
},
"label-param-editor": {
"loadingMessage-loading-labels": "ラベルを読み込み中",
"noOptionsMessage-no-labels-found": "ラベルが見つかりません"
},
"metric-combobox": {
"async-select": {
"aria-label-open-metrics-explorer": "メトリックエクスプローラーを開く",
"placeholder-select-metric": "メトリックを選択",
"tooltip-open-metrics-explorer": "メトリックエクスプローラーを開く"
},
"label-metric": "メトリック",
"tooltip-metric": "オプション:指定されたメトリクスのラベル名に対するラベル値のリストを返します。"
},
"metrics-modal": {
"aria-label-browse-metrics": "メトリックを参照",
"currently-selected": "現在選択中:{{selected}}",
"metrics-pre-filtered": "これらのメトリックは、ラベルフィルターで選択されたラベルによって事前にフィルタリングされています。",
"title-metrics-explorer": "メトリックエクスプローラー"
},
"nested-query": {
"label": {
"ignoring": "無視",
"on": "オン"
},
"operator": "オペレーター",
"tooltip-remove-match": "一致を削除",
"vector-matches": "ベクトル一致"
},
"operation-editor": {
"not-found": "操作{{id}}が見つかりません",
"title-remove": "{{name}}を削除"
},
"operation-header": {
"placeholder-replace-with": "次で置き換え",
"title-click-to-view-alternative-operations": "クリックして代わりの操作を表示",
"title-remove-operation": "操作を削除"
},
"operation-info-button": {
"title-click-to-show-description": "クリックして説明を表示",
"title-remove-operation": "操作を削除"
},
"operation-list": {
"operations": "操作",
"placeholder-search": "検索",
"title-add-operation": "操作を追加"
},
"operation-param-editor": {
"title-add": "{{name}}を追加",
"title-remove": "{{name}}を削除"
},
"operation-utils": {
"getAggregationExplainer": {
"label-by": "{{labelWord}}{{labels}}を保持しながら、次元間で{{aggregationName}}を計算します。",
"label-default": "次元間で{{aggregationName}}を計算します。",
"label-without": "{{labels}}次元間で{{aggregationName}}を計算します。他のすべてのラベルは保持されます。"
}
},
"prom-query-builder-options": {
"aria-label-exemplars": "エグゼンプラーの切り替え。",
"aria-label-format": "形式コンボボックス",
"aria-label-lower-limit-parameter": "最小ステップテキストボックス、ステップパラメーターの下限を設定",
"aria-label-select-resolution": "解像度を選択",
"aria-label-type": "タイプラジオボタングループ",
"format-options": {
"label-heatmap": "ヒートマップ",
"label-table": "テーブル",
"label-time-series": "時系列"
},
"label-exemplars": "エグゼンプラー",
"label-format": "形式",
"label-min-step": "最小ステップ",
"label-resolution": "解像度",
"label-type": "種類",
"placeholder-auto": "自動",
"title-options": "オプション",
"tooltip-min-step": "Prometheusクエリのステップパラメーターと<2>{{interval}}</2>および<4>{{rateInterval}}</4>変数の追加下限。"
},
"prom-query-editor-selector": {
"body-syntax-error": "構文エラーが発生しているか、ビルダーモードに切り替えたときにクエリ構造を視覚化できません。クエリの一部が失われる可能性があります。",
"confirmText-continue": "続行",
"kick-start-your-query": "クエリを開始",
"label-explain": "説明",
"run-queries": "クエリを実行",
"title-parsing-error-switch-builder": "解析エラー:ビルダーモードに切り替えますか?"
},
"prom-query-legend-editor": {
"aria-label-legend": "凡例コンボボックス",
"label-legend": "凡例",
"placeholder-select-legend-mode": "凡例モードを選択",
"tooltip-legend": "系列名の上書きまたはテンプレート。例:{{templateExample}}は{{labelName}}のラベル値に置き換えられます。"
},
"query-builder-hints": {
"hint-details": "ヒント:{{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "ビルダー",
"label-code": "コード"
}
},
"query-pattern": {
"apply-query": "クエリを適用",
"aria-label-apply-query-starter-button": "クエリ適用開始ボタン",
"aria-label-back-button": "戻るボタン",
"aria-label-create-new-query-button": "新しいクエリ作成ボタン",
"aria-label-raw-query": "{{patternName}}の生クエリ",
"aria-label-use-this-query-button": "このクエリボタンを使用",
"back": "戻る",
"create-new-query": "新しいクエリを作成",
"use-this-query": "このクエリを使用"
},
"query-patterns-modal": {
"aria-label-close-kick-start-your-query-modal": "クエリを開始モーダルを閉じる",
"aria-label-kick-start-your-query-modal": "クエリモーダルを開始",
"aria-label-toggle-query-starter": "{{patternType}}クエリ開始カードを開いて閉じる",
"close": "閉じる",
"description-kick-start-your-query": "これらのクエリの1つを選択してクエリを開始してください。その後、クエリを完了できます。",
"label-toggle-query-starter": "{{patternType}}クエリスターター",
"title-kick-start-your-query": "クエリを開始"
},
"raw-query": {
"aria-label-selector": "セレクター"
},
"results-table": {
"content-descriptive-type": "{{descriptiveType}}を作成する際、Prometheusはタイプカウンターで複数の系列を公開します。",
"description": "説明",
"message-expand-label-filters": "メトリックが見つかりません。ラベルフィルターを拡張してみてください。",
"message-expand-search": "メトリックが見つかりません。検索とフィルターを拡張してみてください。",
"message-no-metrics-found": "データソース内にメトリックが見つかりません。",
"name": "名前",
"type": "種類"
}
}
}
}

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