├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── Magefile.go ├── README.md ├── README_EN.md ├── dist ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gpx_log-service-datasource_darwin_amd64 ├── gpx_log-service-datasource_darwin_arm64 ├── gpx_log-service-datasource_linux_amd64 ├── gpx_log-service-datasource_linux_arm ├── gpx_log-service-datasource_linux_arm64 ├── gpx_log-service-datasource_windows_amd64.exe ├── img │ └── sls_logo.jpg ├── index.html ├── module.js ├── module.js.LICENSE.txt ├── module.js.map └── plugin.json ├── go.mod ├── go.sum ├── img ├── 2.33 │ ├── config_editor_2.33.png │ └── query_editor_2.33.png ├── 2.34 │ └── totalLogs_2.34.png ├── 2.36 │ ├── custom.jpg │ ├── region_2.36.png │ └── variable_2.36.jpg ├── demo1.png ├── demo2.png ├── demo3.png └── demo4.png ├── jest.config.js ├── makefile ├── old_README.md ├── old_README_CN.md ├── package.json ├── pkg ├── main.go ├── models.go ├── ram.go ├── resource.go ├── sls-plugin.go └── sts.go ├── src ├── ConfigEditor.tsx ├── QueryEditor.tsx ├── SLS-monaco-editor │ ├── MonacoQueryField.tsx │ ├── MonacoQueryFieldOld.tsx │ ├── MonacoQueryFieldProps.ts │ ├── getOverrideServices.ts │ └── language-definition │ │ ├── definition.ts │ │ ├── sls.ts │ │ └── slsterms.ts ├── SelectTips.tsx ├── VariableQueryEditor.tsx ├── components │ ├── Collapse.tsx │ ├── IconButton.tsx │ ├── QueryEditor │ │ ├── EditorField.tsx │ │ ├── EditorRow.tsx │ │ ├── Field.tsx │ │ ├── FieldValidationMessage.tsx │ │ ├── InlineField.tsx │ │ ├── Label.tsx │ │ ├── QueryOptionGroup.tsx │ │ ├── SecretInput.tsx │ │ ├── Space.tsx │ │ └── Stack.tsx │ ├── RadioButtonGroup │ │ ├── RadioButton.tsx │ │ └── RadioButtonGroup.tsx │ ├── Switch │ │ └── switch.tsx │ ├── configSection.tsx │ ├── description.tsx │ └── style.css ├── const.ts ├── custom-header │ ├── custom-header.tsx │ └── custom-headers.tsx ├── datasource.ts ├── img │ └── sls_logo.jpg ├── module.ts ├── plugin.json └── types.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /node_modules/ 3 | .DS_Store 4 | /coverage/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@grafana/toolkit/src/config/prettier.plugin.config.json'), 3 | }; 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # 2.38 (2025-03-03) 4 | - 修复时序库跳转 SLS 路径问题。 5 | - 日志输出级别整理,减少无用输出。 6 | - 支持‘增强Sql’查询,参考 [开启SQL独享版](https://help.aliyun.com/zh/sls/user-guide/enable-dedicated-sql/?spm=a2c4g.11186623.0.0.2c7771dclfhQou) 7 | 8 | # 2.37 (2025-01-13) 9 | 10 | - 修复 grafana 内部初始化 currentPage 的问题。 11 | - 修复自定义 logstore 在日志库列表展示不回显的问题;修复 gotoSLS 跳转到 defaultLogstore 的问题。 12 | 13 | # 2.36 (2024-11-25) 14 | 15 | - 支持 v4 签名,提供更高的安全性。 16 | - 数据源配置页面新增 Region 字段,兼容非受信环境独立签名。 17 | ![region字段](./img/2.36/region_2.36.png) 18 | - 变量替换支持自定义'日志库列表'选项。 19 | - Variables 设置页面选择 'Custom' 类型。'name' 字段作为独立标识,必须包含 logstore 字符串,不区分大小写。'Custom options' 手动填写可选变量,以逗号区分。 20 | ![variable配置](./img/2.36/variable_2.36.jpg) 21 | - Panel 界面修改'日志库列表'选项为自定义Variable,修改相应变量并刷新仪表盘,即可获得最新结果。 22 | ![自定义logstore](./img/2.36/custom.jpg) 23 | 24 | 25 | # 2.35 (2024-10-21) 26 | 27 | 优化查询分析接口性能。 28 | 29 | # 2.34 (2024-09-23) 30 | 31 | - 优化 table/log 视图 32 | - 支持当存在纳秒字段时, time 按纳秒精度排序。 33 | - 支持修改总查询记录数 totalLogs, 默认值为 100, 最小值为 1, 最大值为 5000, 仅对 Query 查询语句生效, 分析语句无效 34 | ![image.png](./img/2.34/totalLogs_2.34.png) 35 | - 修复 gotoSLS 仅可跳转至数据源中设置的默认 Logstore 的问题 36 | - 修复 gotoSLS 跳转后变量替换为具体值的问题 37 | 38 | # 2.33 (2024-09-01) 39 | 40 | - 插件配置界面优化,必填项为 `Endpoint Project AccessKeyID AccessKeySecret`, 不再强制要求填写 `LogStore`;如果不填写 `LogStore`, 请确保你的填写的 Ak 具备当前 Project 的 ListProject 权限,参考 [ListLogStores - 列出 LogStore](https://help.aliyun.com/zh/sls/developer-reference/api-sls-2020-12-30-listlogstores) 41 | - 插件支持自定义 Headers, 但仅在数据源类型为`MetricStore(PrmQL)` 生效 42 | ![图 2](./img/2.33/config_editor_2.33.png) 43 | - 插件编辑界面优化,支持切换数据源类型和在编辑界面下拉选择具体[日志库(Logstore)](https://help.aliyun.com/zh/sls/product-overview/logstore)或[时序库(MetricStore)](https://help.aliyun.com/zh/sls/product-overview/metricstore?spm=a2c4g.11186623.0.0.2e8b60d1YQznSr) 44 | ![图 3](./img/2.33/query_editor_2.33.png) 45 | 46 | - 数据源类型主要是两种语法区别`SQL` 和 `PromQL`,再加上存储库的类型不同,有四种类型可选: `ALL(SQL)`、`Logstore(SQL)`、`MetricStore(SQL)`、`MetricStore(PromQL)` ; 47 | - 日志库(Logstore)支持`SQL`[日志库(Logstore)查询分析](https://help.aliyun.com/zh/sls/user-guide/search-and-analysis); 48 | - 时序库(MetricStore)支持`SQL + PromQL`[时序库(MetricStore)查询分析](https://help.aliyun.com/zh/sls/user-guide/search-and-analysis); 49 | - `MetricStore(PromQL)`支持添加 `custom Headers`,具体在该数据源的配置界面进行添加; 50 | 51 | # 2.32 (2023-04-16) 52 | 53 | - 支持时序图断点展示,通过 time_series(`时间列`, `补全窗口`, `%Y-%m-%d %H:%i:%s`, `null`) 补 null 值进行断点展示 54 | - time*series 参考:[time_series 时间补全函数说明](https://help.aliyun.com/zh/sls/user-guide/date-and-time-functions-1?spm=a2c4g.11186623.0.i10#section-wsz-wt2-4fb) 55 | ![image.png](https://img.alicdn.com/imgextra/i2/O1CN01HzGO5d1Xg48MpyJHS*!!6000000002952-0-tps-3116-1144.jpg) 56 | 57 | # 2.31 (2023-10-18) 58 | 59 | - 支持日志图在 Grafana v10 的展示,日志图可以指定想展示的字段,ycol 格式为`字段1,字段2` 60 | - 流图支持多条线,ycol 格式为`#:#指标1,指标2` 61 | - 修改表格区域的名字为 ycol,使多个表格在同一个图表时,可以区分。 62 | 63 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/182537/1697599988480-6ddc340d-9b64-4531-abbf-748eff4c0df9.png#averageHue=%231e2126&clientId=ue09f9ab6-6409-4&from=paste&height=480&id=u63975600&originHeight=960&originWidth=1630&originalType=binary&ratio=2&rotation=0&showTitle=false&size=363161&status=done&style=none&taskId=u01fdf5fe-b863-44da-b5f8-a3ae4512280&title=&width=815) 64 | 65 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/182537/1697600032996-be73fb93-a181-4f56-a42d-9ebdba626227.png#averageHue=%23202327&clientId=ue09f9ab6-6409-4&from=paste&height=515&id=u8bdbba62&originHeight=1030&originWidth=2130&originalType=binary&ratio=2&rotation=0&showTitle=false&size=562357&status=done&style=none&taskId=uf3ac8398-be19-4a16-abc7-740eebef31c&title=&width=1065) 66 | 67 | - Supports the display of log graphs in Grafana v10. Log graphs can specify the fields you want to display. The ycol format is `Field 1, Field 2` 68 | - The flow graph supports multiple lines, and the ycol format is `#:#Indicator 1,Indicator 2` 69 | - Modify the name of the table area to ycol so that multiple tables can be distinguished when they are in the same chart. 70 | 71 | # 2.30 (2023-07-25) 72 | 73 | - 优化 SLS Grafana 插件后端结构,现支持加入自定义 Resource API 功能。 74 | - 引入 gotoSLS 功能,用户可以方便地跳转到 SLS 控制台进行查询,和体验 SLS 控制台更强大的功能,跳转附带当前 Grafana 的 query、时间信息。在 DataSource 界面配置 roleArn 可实现 STS 跳转,若不配置,则按照正常直接访问逻辑跳转(需要登录控制台)。 75 | - **注意:若配置 STS 跳转,为权限安全考虑,需要满足以下两个条件**: 76 | - **配置 DataSource 的 accessKey 对应的用户,需要有**`**AliyunRAMReadOnlyAccess**`**权限** 77 | - **配置 DataSource 的 roleArn,里面的权限策略,必须有且只有**`**AliyunLogReadOnlyAccess**` 78 | - 原理参考:[控制台内嵌及分享](https://help.aliyun.com/document_detail/74971.html) 79 | - 优化`xcol`的表现形式,现以下拉框的形式规范推荐输入。兼顾兼容性与自定义输入。 80 | - Variable 编辑页面,SLS DataSource 同样引入 Monaco Editor,且自动识别 grafana 版本切换新老显示。 81 | - 修复`xcol`与`ycol`输入框末尾 Tips 在 Grafana 8.3.x 及以下无法显示的问题。 82 | - 修复部分适配问题。 83 | 84 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/21832175/1690257487839-24ccca2d-4fad-4011-9f18-300ceb876a26.png#averageHue=%231d2023&clientId=ud2e482ee-2fd6-4&from=paste&height=523&id=uc82855a6&originHeight=523&originWidth=1723&originalType=binary&ratio=1&rotation=0&showTitle=false&size=531654&status=done&style=none&taskId=u6031a614-f570-4a7b-a959-f1fda0001c8&title=&width=1723)
![image.png](https://cdn.nlark.com/yuque/0/2023/png/21832175/1690257391475-dae60eef-2191-42ab-90e7-d79b5319dfcd.png#averageHue=%231c1f21&clientId=ud2e482ee-2fd6-4&from=paste&height=349&id=u2c395060&originHeight=349&originWidth=1719&originalType=binary&ratio=1&rotation=0&showTitle=false&size=366996&status=done&style=none&taskId=u22e237d7-d4ad-4a9f-aae2-8ce25dc0915&title=&width=1719) 85 | 86 | - Optimize the back-end structure of the SLS Grafana plug-in, and now support the addition of custom Resource API functions. 87 | - Introducing the gotoSLS function, users can easily jump to the SLS console to query and experience more powerful functions of the SLS console, and the jump includes the current Grafana query and time information. Configuring roleArn on the DataSource interface can realize STS jumping. If not configured, it will jump according to the normal direct access logic (login to the console is required). 88 | - **Note: If STS redirection is configured, the following two conditions need to be met for permission security considerations:** 89 | - **To configure the user corresponding to the accessKey of the DataSource, `AliyunRAMReadOnlyAccess` permission is required** 90 | - **Configure the roleArn of DataSource, the permission policy inside must have and only `AliyunLogReadOnlyAccess`** 91 | - Principle reference: [Console embedding and sharing](https://help.aliyun.com/document_detail/74971.html) 92 | - Optimize the expression form of `xcol`, and now standardize the recommended input in the form of a drop-down box. Take into account compatibility and custom input. 93 | - On the Variable editing page, SLS DataSource also introduces Monaco Editor, and automatically recognizes the grafana version to switch between the new and old displays. 94 | - Fix the problem that Tips at the end of the `xcol` and `ycol` input boxes cannot be displayed in Grafana 8.3.x and below. 95 | - Fix some adaptation problems. 96 | 97 | # 2.29 (2023-06-29) 98 | 99 | - 全新 SLS Query 编写体验,引入 Monaco Editor,支持语法高亮显示,关键字、函数自动提示等功能。 100 | - SLS Query 框支持多行自动换行和高度扩展。 101 | - `xcol`与`ycol`的输入使用 Grafana 的 Input 组件,带来更一致的体验。 102 | - `xcol`与`ycol`输入框末尾增加 Tips,介绍常见写法,方便初学者上手。 103 | - 修复一系列版本问题。 104 | 105 |
106 | 107 | - Brand-new SLS Query writing experience, introduces Monaco Editor, supports syntax highlighting, keyword, function automatic prompting and other functions. 108 | - The SLS Query box supports multi-line word wrapping and height expansion. 109 | - The input of xcol and ycol uses Grafana's Input component to bring a more consistent experience. 110 | - Tips are added at the end of the xcol and ycol input boxes to introduce common writing methods, which is convenient for beginners. 111 | - Fix a series of version issues. 112 | 113 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/21832175/1687953008367-f863ccc6-dcf5-4998-bcbc-c18aec0ac059.png#averageHue=%231e2024&clientId=u72ccc8ce-757a-4&from=paste&height=347&id=u3586dcad&originHeight=347&originWidth=1216&originalType=binary&ratio=1&rotation=0&showTitle=false&size=294424&status=done&style=none&taskId=u73ee0591-f289-4f53-8886-1a93e4914e5&title=&width=1216)
![image.png](https://cdn.nlark.com/yuque/0/2023/png/21832175/1687953284855-86c636fb-caa6-4163-ab23-0478cfb55b73.png#averageHue=%2323252a&clientId=u72ccc8ce-757a-4&from=paste&height=270&id=u4ec3dd89&originHeight=270&originWidth=564&originalType=binary&ratio=1&rotation=0&showTitle=false&size=112573&status=done&style=none&taskId=uac6a4fe0-65ca-4df2-8a4e-5faa1ea911a&title=&width=564) 114 | 115 | # 2.28 116 | 117 | - 适配 Grafana 9.4 及以上版本的 Datasource 数据源结构。 118 |
119 | - Adapt to the Datasource data source structure of Grafana 9.4 and above. 120 | 121 | # 2.27 122 | 123 | - 加了 legacy_compatible 设置,适配旧版本的返回结构(带 response) 124 |
125 | - Added legacy_compatible to adapt to the return structure of the old version (with response) 126 | 127 | # 2.0.0 (released) 128 | 129 | Initial release. 130 | 131 | Support Grafana 8.0. 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Grafana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Magefile.go: -------------------------------------------------------------------------------- 1 | //+build mage 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | // mage:import 8 | build "github.com/grafana/grafana-plugin-sdk-go/build" 9 | ) 10 | 11 | // Hello prints a message (shows that you can define custom Mage targets). 12 | func Hello() { 13 | fmt.Println("hello plugin developer!") 14 | } 15 | 16 | // Default configures the default target. 17 | var Default = build.BuildAll 18 | -------------------------------------------------------------------------------- /dist/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # 2.38 (2025-03-03) 4 | - 修复时序库跳转 SLS 路径问题。 5 | - 日志输出级别整理,减少无用输出。 6 | - 支持‘增强Sql’查询,参考 [开启SQL独享版](https://help.aliyun.com/zh/sls/user-guide/enable-dedicated-sql/?spm=a2c4g.11186623.0.0.2c7771dclfhQou) 7 | 8 | # 2.37 (2025-01-13) 9 | 10 | - 修复 grafana 内部初始化 currentPage 的问题。 11 | - 修复自定义 logstore 在日志库列表展示不回显的问题;修复 gotoSLS 跳转到 defaultLogstore 的问题。 12 | 13 | # 2.36 (2024-11-25) 14 | 15 | - 支持 v4 签名,提供更高的安全性。 16 | - 数据源配置页面新增 Region 字段,兼容非受信环境独立签名。 17 | ![region字段](./img/2.36/region_2.36.png) 18 | - 变量替换支持自定义'日志库列表'选项。 19 | - Variables 设置页面选择 'Custom' 类型。'name' 字段作为独立标识,必须包含 logstore 字符串,不区分大小写。'Custom options' 手动填写可选变量,以逗号区分。 20 | ![variable配置](./img/2.36/variable_2.36.jpg) 21 | - Panel 界面修改'日志库列表'选项为自定义Variable,修改相应变量并刷新仪表盘,即可获得最新结果。 22 | ![自定义logstore](./img/2.36/custom.jpg) 23 | 24 | 25 | # 2.35 (2024-10-21) 26 | 27 | 优化查询分析接口性能。 28 | 29 | # 2.34 (2024-09-23) 30 | 31 | - 优化 table/log 视图 32 | - 支持当存在纳秒字段时, time 按纳秒精度排序。 33 | - 支持修改总查询记录数 totalLogs, 默认值为 100, 最小值为 1, 最大值为 5000, 仅对 Query 查询语句生效, 分析语句无效 34 | ![image.png](./img/2.34/totalLogs_2.34.png) 35 | - 修复 gotoSLS 仅可跳转至数据源中设置的默认 Logstore 的问题 36 | - 修复 gotoSLS 跳转后变量替换为具体值的问题 37 | 38 | # 2.33 (2024-09-01) 39 | 40 | - 插件配置界面优化,必填项为 `Endpoint Project AccessKeyID AccessKeySecret`, 不再强制要求填写 `LogStore`;如果不填写 `LogStore`, 请确保你的填写的 Ak 具备当前 Project 的 ListProject 权限,参考 [ListLogStores - 列出 LogStore](https://help.aliyun.com/zh/sls/developer-reference/api-sls-2020-12-30-listlogstores) 41 | - 插件支持自定义 Headers, 但仅在数据源类型为`MetricStore(PrmQL)` 生效 42 | ![图 2](./img/2.33/config_editor_2.33.png) 43 | - 插件编辑界面优化,支持切换数据源类型和在编辑界面下拉选择具体[日志库(Logstore)](https://help.aliyun.com/zh/sls/product-overview/logstore)或[时序库(MetricStore)](https://help.aliyun.com/zh/sls/product-overview/metricstore?spm=a2c4g.11186623.0.0.2e8b60d1YQznSr) 44 | ![图 3](./img/2.33/query_editor_2.33.png) 45 | 46 | - 数据源类型主要是两种语法区别`SQL` 和 `PromQL`,再加上存储库的类型不同,有四种类型可选: `ALL(SQL)`、`Logstore(SQL)`、`MetricStore(SQL)`、`MetricStore(PromQL)` ; 47 | - 日志库(Logstore)支持`SQL`[日志库(Logstore)查询分析](https://help.aliyun.com/zh/sls/user-guide/search-and-analysis); 48 | - 时序库(MetricStore)支持`SQL + PromQL`[时序库(MetricStore)查询分析](https://help.aliyun.com/zh/sls/user-guide/search-and-analysis); 49 | - `MetricStore(PromQL)`支持添加 `custom Headers`,具体在该数据源的配置界面进行添加; 50 | 51 | # 2.32 (2023-04-16) 52 | 53 | - 支持时序图断点展示,通过 time_series(`时间列`, `补全窗口`, `%Y-%m-%d %H:%i:%s`, `null`) 补 null 值进行断点展示 54 | - time*series 参考:[time_series 时间补全函数说明](https://help.aliyun.com/zh/sls/user-guide/date-and-time-functions-1?spm=a2c4g.11186623.0.i10#section-wsz-wt2-4fb) 55 | ![image.png](https://img.alicdn.com/imgextra/i2/O1CN01HzGO5d1Xg48MpyJHS*!!6000000002952-0-tps-3116-1144.jpg) 56 | 57 | # 2.31 (2023-10-18) 58 | 59 | - 支持日志图在 Grafana v10 的展示,日志图可以指定想展示的字段,ycol 格式为`字段1,字段2` 60 | - 流图支持多条线,ycol 格式为`#:#指标1,指标2` 61 | - 修改表格区域的名字为 ycol,使多个表格在同一个图表时,可以区分。 62 | 63 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/182537/1697599988480-6ddc340d-9b64-4531-abbf-748eff4c0df9.png#averageHue=%231e2126&clientId=ue09f9ab6-6409-4&from=paste&height=480&id=u63975600&originHeight=960&originWidth=1630&originalType=binary&ratio=2&rotation=0&showTitle=false&size=363161&status=done&style=none&taskId=u01fdf5fe-b863-44da-b5f8-a3ae4512280&title=&width=815) 64 | 65 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/182537/1697600032996-be73fb93-a181-4f56-a42d-9ebdba626227.png#averageHue=%23202327&clientId=ue09f9ab6-6409-4&from=paste&height=515&id=u8bdbba62&originHeight=1030&originWidth=2130&originalType=binary&ratio=2&rotation=0&showTitle=false&size=562357&status=done&style=none&taskId=uf3ac8398-be19-4a16-abc7-740eebef31c&title=&width=1065) 66 | 67 | - Supports the display of log graphs in Grafana v10. Log graphs can specify the fields you want to display. The ycol format is `Field 1, Field 2` 68 | - The flow graph supports multiple lines, and the ycol format is `#:#Indicator 1,Indicator 2` 69 | - Modify the name of the table area to ycol so that multiple tables can be distinguished when they are in the same chart. 70 | 71 | # 2.30 (2023-07-25) 72 | 73 | - 优化 SLS Grafana 插件后端结构,现支持加入自定义 Resource API 功能。 74 | - 引入 gotoSLS 功能,用户可以方便地跳转到 SLS 控制台进行查询,和体验 SLS 控制台更强大的功能,跳转附带当前 Grafana 的 query、时间信息。在 DataSource 界面配置 roleArn 可实现 STS 跳转,若不配置,则按照正常直接访问逻辑跳转(需要登录控制台)。 75 | - **注意:若配置 STS 跳转,为权限安全考虑,需要满足以下两个条件**: 76 | - **配置 DataSource 的 accessKey 对应的用户,需要有**`**AliyunRAMReadOnlyAccess**`**权限** 77 | - **配置 DataSource 的 roleArn,里面的权限策略,必须有且只有**`**AliyunLogReadOnlyAccess**` 78 | - 原理参考:[控制台内嵌及分享](https://help.aliyun.com/document_detail/74971.html) 79 | - 优化`xcol`的表现形式,现以下拉框的形式规范推荐输入。兼顾兼容性与自定义输入。 80 | - Variable 编辑页面,SLS DataSource 同样引入 Monaco Editor,且自动识别 grafana 版本切换新老显示。 81 | - 修复`xcol`与`ycol`输入框末尾 Tips 在 Grafana 8.3.x 及以下无法显示的问题。 82 | - 修复部分适配问题。 83 | 84 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/21832175/1690257487839-24ccca2d-4fad-4011-9f18-300ceb876a26.png#averageHue=%231d2023&clientId=ud2e482ee-2fd6-4&from=paste&height=523&id=uc82855a6&originHeight=523&originWidth=1723&originalType=binary&ratio=1&rotation=0&showTitle=false&size=531654&status=done&style=none&taskId=u6031a614-f570-4a7b-a959-f1fda0001c8&title=&width=1723)
![image.png](https://cdn.nlark.com/yuque/0/2023/png/21832175/1690257391475-dae60eef-2191-42ab-90e7-d79b5319dfcd.png#averageHue=%231c1f21&clientId=ud2e482ee-2fd6-4&from=paste&height=349&id=u2c395060&originHeight=349&originWidth=1719&originalType=binary&ratio=1&rotation=0&showTitle=false&size=366996&status=done&style=none&taskId=u22e237d7-d4ad-4a9f-aae2-8ce25dc0915&title=&width=1719) 85 | 86 | - Optimize the back-end structure of the SLS Grafana plug-in, and now support the addition of custom Resource API functions. 87 | - Introducing the gotoSLS function, users can easily jump to the SLS console to query and experience more powerful functions of the SLS console, and the jump includes the current Grafana query and time information. Configuring roleArn on the DataSource interface can realize STS jumping. If not configured, it will jump according to the normal direct access logic (login to the console is required). 88 | - **Note: If STS redirection is configured, the following two conditions need to be met for permission security considerations:** 89 | - **To configure the user corresponding to the accessKey of the DataSource, `AliyunRAMReadOnlyAccess` permission is required** 90 | - **Configure the roleArn of DataSource, the permission policy inside must have and only `AliyunLogReadOnlyAccess`** 91 | - Principle reference: [Console embedding and sharing](https://help.aliyun.com/document_detail/74971.html) 92 | - Optimize the expression form of `xcol`, and now standardize the recommended input in the form of a drop-down box. Take into account compatibility and custom input. 93 | - On the Variable editing page, SLS DataSource also introduces Monaco Editor, and automatically recognizes the grafana version to switch between the new and old displays. 94 | - Fix the problem that Tips at the end of the `xcol` and `ycol` input boxes cannot be displayed in Grafana 8.3.x and below. 95 | - Fix some adaptation problems. 96 | 97 | # 2.29 (2023-06-29) 98 | 99 | - 全新 SLS Query 编写体验,引入 Monaco Editor,支持语法高亮显示,关键字、函数自动提示等功能。 100 | - SLS Query 框支持多行自动换行和高度扩展。 101 | - `xcol`与`ycol`的输入使用 Grafana 的 Input 组件,带来更一致的体验。 102 | - `xcol`与`ycol`输入框末尾增加 Tips,介绍常见写法,方便初学者上手。 103 | - 修复一系列版本问题。 104 | 105 |
106 | 107 | - Brand-new SLS Query writing experience, introduces Monaco Editor, supports syntax highlighting, keyword, function automatic prompting and other functions. 108 | - The SLS Query box supports multi-line word wrapping and height expansion. 109 | - The input of xcol and ycol uses Grafana's Input component to bring a more consistent experience. 110 | - Tips are added at the end of the xcol and ycol input boxes to introduce common writing methods, which is convenient for beginners. 111 | - Fix a series of version issues. 112 | 113 | ![image.png](https://cdn.nlark.com/yuque/0/2023/png/21832175/1687953008367-f863ccc6-dcf5-4998-bcbc-c18aec0ac059.png#averageHue=%231e2024&clientId=u72ccc8ce-757a-4&from=paste&height=347&id=u3586dcad&originHeight=347&originWidth=1216&originalType=binary&ratio=1&rotation=0&showTitle=false&size=294424&status=done&style=none&taskId=u73ee0591-f289-4f53-8886-1a93e4914e5&title=&width=1216)
![image.png](https://cdn.nlark.com/yuque/0/2023/png/21832175/1687953284855-86c636fb-caa6-4163-ab23-0478cfb55b73.png#averageHue=%2323252a&clientId=u72ccc8ce-757a-4&from=paste&height=270&id=u4ec3dd89&originHeight=270&originWidth=564&originalType=binary&ratio=1&rotation=0&showTitle=false&size=112573&status=done&style=none&taskId=uac6a4fe0-65ca-4df2-8a4e-5faa1ea911a&title=&width=564) 114 | 115 | # 2.28 116 | 117 | - 适配 Grafana 9.4 及以上版本的 Datasource 数据源结构。 118 |
119 | - Adapt to the Datasource data source structure of Grafana 9.4 and above. 120 | 121 | # 2.27 122 | 123 | - 加了 legacy_compatible 设置,适配旧版本的返回结构(带 response) 124 |
125 | - Added legacy_compatible to adapt to the return structure of the old version (with response) 126 | 127 | # 2.0.0 (released) 128 | 129 | Initial release. 130 | 131 | Support Grafana 8.0. 132 | -------------------------------------------------------------------------------- /dist/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Grafana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/gpx_log-service-datasource_darwin_amd64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_darwin_amd64 -------------------------------------------------------------------------------- /dist/gpx_log-service-datasource_darwin_arm64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_darwin_arm64 -------------------------------------------------------------------------------- /dist/gpx_log-service-datasource_linux_amd64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_linux_amd64 -------------------------------------------------------------------------------- /dist/gpx_log-service-datasource_linux_arm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_linux_arm -------------------------------------------------------------------------------- /dist/gpx_log-service-datasource_linux_arm64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_linux_arm64 -------------------------------------------------------------------------------- /dist/gpx_log-service-datasource_windows_amd64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_windows_amd64.exe -------------------------------------------------------------------------------- /dist/img/sls_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/img/sls_logo.jpg -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Webpack App 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /dist/module.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 2 | -------------------------------------------------------------------------------- /dist/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json", 3 | "type": "datasource", 4 | "name": "log-service-datasource", 5 | "id": "aliyun-log-service-datasource", 6 | "metrics": true, 7 | "backend": true, 8 | "logs": true, 9 | "alerting": true, 10 | "executable": "gpx_log-service-datasource", 11 | "flow_chart_max_points": "6000000", 12 | "legacy_compatible": false, 13 | "info": { 14 | "description": "Aliyun log service datasource (backend version)", 15 | "author": { 16 | "name": "aliyun-log", 17 | "url": "https://aliyun.com/product/sls" 18 | }, 19 | "keywords": [ 20 | "sls", 21 | "aliyun", 22 | "log", 23 | "日志服务" 24 | ], 25 | "logos": { 26 | "small": "img/sls_logo.jpg", 27 | "large": "img/sls_logo.jpg" 28 | }, 29 | "links": [ 30 | { 31 | "name": "Website", 32 | "url": "https://github.com/aliyun/aliyun-log-grafana-datasource-plugin" 33 | }, 34 | { 35 | "name": "License", 36 | "url": "https://github.com/aliyun/aliyun-log-grafana-datasource-plugin/blob/master/LICENSE" 37 | } 38 | ], 39 | "screenshots": [], 40 | "version": "2.38", 41 | "updated": "2025-03-05" 42 | }, 43 | "dependencies": { 44 | "grafanaDependency": ">=7.0.0", 45 | "plugins": [] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/grafana-starter-datasource-backend 2 | 3 | go 1.21 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/aliyun/alibaba-cloud-sdk-go v1.62.453 9 | github.com/aliyun/aliyun-log-go-sdk v0.1.83 10 | github.com/grafana/grafana-plugin-sdk-go v0.102.0 11 | github.com/satori/go.uuid v1.2.0 12 | ) 13 | 14 | require ( 15 | cloud.google.com/go v0.65.0 // indirect 16 | cloud.google.com/go/bigquery v1.8.0 // indirect 17 | cloud.google.com/go/datastore v1.1.0 // indirect 18 | cloud.google.com/go/pubsub v1.3.1 // indirect 19 | cloud.google.com/go/storage v1.10.0 // indirect 20 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 // indirect 21 | github.com/BurntSushi/toml v1.2.1 // indirect 22 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 // indirect 23 | github.com/DataDog/datadog-go v3.2.0+incompatible // indirect 24 | github.com/DataDog/zstd v1.5.5 // indirect 25 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 26 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect 27 | github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d // indirect 28 | github.com/OneOfOne/xxhash v1.2.2 // indirect 29 | github.com/Shopify/sarama v1.19.0 // indirect 30 | github.com/Shopify/toxiproxy v2.1.4+incompatible // indirect 31 | github.com/VividCortex/gohistogram v1.0.0 // indirect 32 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect 33 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af // indirect 34 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 35 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 36 | github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect 37 | github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.4 // indirect 38 | github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect 39 | github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect 40 | github.com/alibabacloud-go/openapi-util v0.1.0 // indirect 41 | github.com/alibabacloud-go/sts-20150401/v2 v2.0.1 // indirect 42 | github.com/alibabacloud-go/tea v1.1.19 // indirect 43 | github.com/alibabacloud-go/tea-utils v1.3.1 // indirect 44 | github.com/alibabacloud-go/tea-utils/v2 v2.0.1 // indirect 45 | github.com/alibabacloud-go/tea-xml v1.1.2 // indirect 46 | github.com/aliyun/credentials-go v1.1.2 // indirect 47 | github.com/antihax/optional v1.0.0 // indirect 48 | github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91 // indirect 49 | github.com/apache/thrift v0.13.0 // indirect 50 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e // indirect 51 | github.com/armon/go-metrics v0.4.0 // indirect 52 | github.com/armon/go-radix v1.0.0 // indirect 53 | github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a // indirect 54 | github.com/aws/aws-lambda-go v1.13.3 // indirect 55 | github.com/aws/aws-sdk-go v1.40.45 // indirect 56 | github.com/aws/aws-sdk-go-v2 v1.9.1 // indirect 57 | github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 // indirect 58 | github.com/aws/smithy-go v1.8.0 // indirect 59 | github.com/benbjohnson/clock v1.1.0 // indirect 60 | github.com/beorn7/perks v1.0.1 // indirect 61 | github.com/bgentry/speakeasy v0.1.0 // indirect 62 | github.com/casbin/casbin/v2 v2.37.0 // indirect 63 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect 64 | github.com/cenkalti/backoff/v4 v4.1.1 // indirect 65 | github.com/census-instrumentation/opencensus-proto v0.2.1 // indirect 66 | github.com/cespare/xxhash v1.1.0 // indirect 67 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 68 | github.com/cheekybits/genny v1.0.0 // indirect 69 | github.com/chzyer/logex v1.1.10 // indirect 70 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 71 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect 72 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect 73 | github.com/circonus-labs/circonusllhist v0.1.3 // indirect 74 | github.com/clbanning/mxj v1.8.4 // indirect 75 | github.com/clbanning/mxj/v2 v2.5.5 // indirect 76 | github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec // indirect 77 | github.com/client9/misspell v0.3.4 // indirect 78 | github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 // indirect 79 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 // indirect 80 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed // indirect 81 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa // indirect 82 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect 83 | github.com/coreos/go-semver v0.3.0 // indirect 84 | github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 // indirect 85 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 86 | github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf // indirect 87 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 88 | github.com/creack/pty v1.1.9 // indirect 89 | github.com/davecgh/go-spew v1.1.1 // indirect 90 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 91 | github.com/duke-git/lancet v1.3.7 // indirect 92 | github.com/dustin/go-humanize v1.0.0 // indirect 93 | github.com/eapache/go-resiliency v1.1.0 // indirect 94 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect 95 | github.com/eapache/queue v1.1.0 // indirect 96 | github.com/edsrzf/mmap-go v1.0.0 // indirect 97 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0 // indirect 98 | github.com/envoyproxy/protoc-gen-validate v0.1.0 // indirect 99 | github.com/fatih/color v1.13.0 // indirect 100 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 // indirect 101 | github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 // indirect 102 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect 103 | github.com/frankban/quicktest v1.10.2 // indirect 104 | github.com/fsnotify/fsnotify v1.4.9 // indirect 105 | github.com/ghodss/yaml v1.0.0 // indirect 106 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 // indirect 107 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 // indirect 108 | github.com/go-kit/kit v0.13.0 // indirect 109 | github.com/go-kit/log v0.2.1 // indirect 110 | github.com/go-logfmt/logfmt v0.6.0 // indirect 111 | github.com/go-sql-driver/mysql v1.4.0 // indirect 112 | github.com/go-stack/stack v1.8.0 // indirect 113 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 114 | github.com/go-zookeeper/zk v1.0.2 // indirect 115 | github.com/godbus/dbus/v5 v5.0.4 // indirect 116 | github.com/gogo/googleapis v1.1.0 // indirect 117 | github.com/gogo/protobuf v1.3.2 // indirect 118 | github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d // indirect 119 | github.com/golang-jwt/jwt/v4 v4.0.0 // indirect 120 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 121 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 122 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 123 | github.com/golang/mock v1.4.4 // indirect 124 | github.com/golang/protobuf v1.5.4 // indirect 125 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect 126 | github.com/google/btree v1.0.0 // indirect 127 | github.com/google/flatbuffers v1.11.0 // indirect 128 | github.com/google/go-cmp v0.6.0 // indirect 129 | github.com/google/gofuzz v1.0.0 // indirect 130 | github.com/google/martian v2.1.0+incompatible // indirect 131 | github.com/google/martian/v3 v3.0.0 // indirect 132 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 // indirect 133 | github.com/google/renameio v0.1.0 // indirect 134 | github.com/google/uuid v1.1.2 // indirect 135 | github.com/googleapis/gax-go/v2 v2.0.5 // indirect 136 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect 137 | github.com/gorilla/context v1.1.1 // indirect 138 | github.com/gorilla/mux v1.8.0 // indirect 139 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c // indirect 140 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 141 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 142 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 143 | github.com/hashicorp/consul/api v1.14.0 // indirect 144 | github.com/hashicorp/consul/sdk v0.10.0 // indirect 145 | github.com/hashicorp/errwrap v1.0.0 // indirect 146 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 147 | github.com/hashicorp/go-hclog v1.2.2 // indirect 148 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 149 | github.com/hashicorp/go-msgpack v0.5.5 // indirect 150 | github.com/hashicorp/go-multierror v1.1.1 // indirect 151 | github.com/hashicorp/go-plugin v1.2.2 // indirect 152 | github.com/hashicorp/go-retryablehttp v0.5.3 // indirect 153 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 154 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 155 | github.com/hashicorp/go-syslog v1.0.0 // indirect 156 | github.com/hashicorp/go-uuid v1.0.2 // indirect 157 | github.com/hashicorp/go-version v1.2.0 // indirect 158 | github.com/hashicorp/go.net v0.0.1 // indirect 159 | github.com/hashicorp/golang-lru v0.5.4 // indirect 160 | github.com/hashicorp/logutils v1.0.0 // indirect 161 | github.com/hashicorp/mdns v1.0.4 // indirect 162 | github.com/hashicorp/memberlist v0.4.0 // indirect 163 | github.com/hashicorp/serf v0.10.0 // indirect 164 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect 165 | github.com/hpcloud/tail v1.0.0 // indirect 166 | github.com/hudl/fargo v1.4.0 // indirect 167 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect 168 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 169 | github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab // indirect 170 | github.com/jhump/protoreflect v1.6.0 // indirect 171 | github.com/jmespath/go-jmespath v0.4.0 // indirect 172 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 // indirect 173 | github.com/jonboulle/clockwork v0.1.0 // indirect 174 | github.com/jpillora/backoff v1.0.0 // indirect 175 | github.com/json-iterator/go v1.1.12 // indirect 176 | github.com/jstemmer/go-junit-report v0.9.1 // indirect 177 | github.com/jtolds/gls v4.20.0+incompatible // indirect 178 | github.com/julienschmidt/httprouter v1.3.0 // indirect 179 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 // indirect 180 | github.com/kisielk/errcheck v1.5.0 // indirect 181 | github.com/kisielk/gotool v1.0.0 // indirect 182 | github.com/klauspost/compress v1.17.11 // indirect 183 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect 184 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect 185 | github.com/kr/pretty v0.2.1 // indirect 186 | github.com/kr/pty v1.1.1 // indirect 187 | github.com/kr/text v0.2.0 // indirect 188 | github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 // indirect 189 | github.com/lightstep/lightstep-tracer-go v0.18.1 // indirect 190 | github.com/lyft/protoc-gen-validate v0.0.13 // indirect 191 | github.com/magefile/mage v1.11.0 // indirect 192 | github.com/mattetti/filebuffer v1.0.1 // indirect 193 | github.com/mattn/go-colorable v0.1.13 // indirect 194 | github.com/mattn/go-isatty v0.0.16 // indirect 195 | github.com/mattn/go-runewidth v0.0.9 // indirect 196 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 197 | github.com/miekg/dns v1.1.43 // indirect 198 | github.com/minio/highwayhash v1.0.2 // indirect 199 | github.com/mitchellh/cli v1.1.0 // indirect 200 | github.com/mitchellh/go-homedir v1.1.0 // indirect 201 | github.com/mitchellh/go-testing-interface v1.0.0 // indirect 202 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 203 | github.com/mitchellh/gox v0.4.0 // indirect 204 | github.com/mitchellh/iochan v1.0.0 // indirect 205 | github.com/mitchellh/mapstructure v1.5.0 // indirect 206 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 207 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 208 | github.com/modern-go/reflect2 v1.0.2 // indirect 209 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 210 | github.com/nats-io/jwt v0.3.2 // indirect 211 | github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a // indirect 212 | github.com/nats-io/nats-server/v2 v2.8.4 // indirect 213 | github.com/nats-io/nats.go v1.15.0 // indirect 214 | github.com/nats-io/nkeys v0.3.0 // indirect 215 | github.com/nats-io/nuid v1.0.1 // indirect 216 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 217 | github.com/nxadm/tail v1.4.8 // indirect 218 | github.com/oklog/oklog v0.3.2 // indirect 219 | github.com/oklog/run v1.0.0 // indirect 220 | github.com/olekukonko/tablewriter v0.0.5 // indirect 221 | github.com/onsi/ginkgo v1.16.2 // indirect 222 | github.com/onsi/gomega v1.13.0 // indirect 223 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect 224 | github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect 225 | github.com/opentracing/basictracer-go v1.0.0 // indirect 226 | github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect 227 | github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 // indirect 228 | github.com/openzipkin/zipkin-go v0.2.5 // indirect 229 | github.com/pact-foundation/pact-go v1.0.4 // indirect 230 | github.com/pascaldekloe/goe v0.1.0 // indirect 231 | github.com/pborman/uuid v1.2.0 // indirect 232 | github.com/performancecopilot/speed v3.0.0+incompatible // indirect 233 | github.com/performancecopilot/speed/v4 v4.0.0 // indirect 234 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 235 | github.com/pkg/errors v0.9.1 // indirect 236 | github.com/pkg/profile v1.2.1 // indirect 237 | github.com/pmezard/go-difflib v1.0.0 // indirect 238 | github.com/posener/complete v1.2.3 // indirect 239 | github.com/prometheus/client_golang v1.11.1 // indirect 240 | github.com/prometheus/client_model v0.2.0 // indirect 241 | github.com/prometheus/common v0.30.0 // indirect 242 | github.com/prometheus/procfs v0.7.3 // indirect 243 | github.com/rabbitmq/amqp091-go v1.2.0 // indirect 244 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect 245 | github.com/rogpeppe/fastuuid v1.2.0 // indirect 246 | github.com/rogpeppe/go-internal v1.3.0 // indirect 247 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 248 | github.com/ryanuber/columnize v2.1.0+incompatible // indirect 249 | github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da // indirect 250 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect 251 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 252 | github.com/sirupsen/logrus v1.8.1 // indirect 253 | github.com/smartystreets/assertions v1.1.0 // indirect 254 | github.com/smartystreets/goconvey v1.6.4 // indirect 255 | github.com/soheilhy/cmux v0.1.4 // indirect 256 | github.com/sony/gobreaker v0.4.1 // indirect 257 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 // indirect 258 | github.com/spf13/cobra v0.0.3 // indirect 259 | github.com/spf13/pflag v1.0.1 // indirect 260 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 // indirect 261 | github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e // indirect 262 | github.com/stretchr/objx v0.4.0 // indirect 263 | github.com/stretchr/testify v1.8.0 // indirect 264 | github.com/tjfoc/gmsm v1.3.2 // indirect 265 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 // indirect 266 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect 267 | github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect 268 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 269 | github.com/urfave/cli v1.22.1 // indirect 270 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect 271 | github.com/yuin/goldmark v1.4.13 // indirect 272 | gitlab.alibaba-inc.com/rapt/go-security-utils v0.1.4 // indirect 273 | go.etcd.io/bbolt v1.3.3 // indirect 274 | go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 // indirect 275 | go.etcd.io/etcd/api/v3 v3.5.0 // indirect 276 | go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect 277 | go.etcd.io/etcd/client/v2 v2.305.0 // indirect 278 | go.etcd.io/etcd/client/v3 v3.5.0 // indirect 279 | go.opencensus.io v0.23.0 // indirect 280 | go.opentelemetry.io/proto/otlp v0.7.0 // indirect 281 | go.uber.org/atomic v1.11.0 // indirect 282 | go.uber.org/goleak v1.1.11 // indirect 283 | go.uber.org/multierr v1.7.0 // indirect 284 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee // indirect 285 | go.uber.org/zap v1.24.0 // indirect 286 | golang.org/x/crypto v0.28.0 // indirect 287 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 // indirect 288 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect 289 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect 290 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 // indirect 291 | golang.org/x/mod v0.17.0 // indirect 292 | golang.org/x/net v0.30.0 // indirect 293 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect 294 | golang.org/x/sync v0.8.0 // indirect 295 | golang.org/x/sys v0.26.0 // indirect 296 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 // indirect 297 | golang.org/x/term v0.25.0 // indirect 298 | golang.org/x/text v0.19.0 // indirect 299 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect 300 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 301 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 302 | gonum.org/v1/gonum v0.8.2 // indirect 303 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 // indirect 304 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b // indirect 305 | google.golang.org/api v0.30.0 // indirect 306 | google.golang.org/appengine v1.6.6 // indirect 307 | google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect 308 | google.golang.org/grpc v1.40.0 // indirect 309 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3 // indirect 310 | google.golang.org/protobuf v1.35.1 // indirect 311 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect 312 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 313 | gopkg.in/cheggaaa/pb.v1 v1.0.25 // indirect 314 | gopkg.in/errgo.v2 v2.1.0 // indirect 315 | gopkg.in/fsnotify.v1 v1.4.7 // indirect 316 | gopkg.in/gcfg.v1 v1.2.3 // indirect 317 | gopkg.in/ini.v1 v1.66.2 // indirect 318 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 319 | gopkg.in/resty.v1 v1.12.0 // indirect 320 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 321 | gopkg.in/warnings.v0 v0.1.2 // indirect 322 | gopkg.in/yaml.v2 v2.4.0 // indirect 323 | gopkg.in/yaml.v3 v3.0.1 // indirect 324 | honnef.co/go/tools v0.0.1-2020.1.4 // indirect 325 | rsc.io/binaryregexp v0.2.0 // indirect 326 | rsc.io/pdf v0.1.1 // indirect 327 | rsc.io/quote/v3 v3.1.0 // indirect 328 | rsc.io/sampler v1.3.0 // indirect 329 | sigs.k8s.io/yaml v1.2.0 // indirect 330 | sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 // indirect 331 | ) 332 | -------------------------------------------------------------------------------- /img/2.33/config_editor_2.33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.33/config_editor_2.33.png -------------------------------------------------------------------------------- /img/2.33/query_editor_2.33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.33/query_editor_2.33.png -------------------------------------------------------------------------------- /img/2.34/totalLogs_2.34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.34/totalLogs_2.34.png -------------------------------------------------------------------------------- /img/2.36/custom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.36/custom.jpg -------------------------------------------------------------------------------- /img/2.36/region_2.36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.36/region_2.36.png -------------------------------------------------------------------------------- /img/2.36/variable_2.36.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.36/variable_2.36.jpg -------------------------------------------------------------------------------- /img/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/demo1.png -------------------------------------------------------------------------------- /img/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/demo2.png -------------------------------------------------------------------------------- /img/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/demo3.png -------------------------------------------------------------------------------- /img/demo4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/demo4.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // This file is needed because it is used by vscode and other tools that 2 | // call `jest` directly. However, unless you are doing anything special 3 | // do not edit this file 4 | 5 | const standard = require('@grafana/toolkit/src/config/jest.plugin.config'); 6 | 7 | // This process will use the same config that `yarn test` is using 8 | module.exports = standard.jestConfig(); 9 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | darwin: 2 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o dist/gpx_log-service-datasource_darwin_amd64 pkg/* 3 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o dist/gpx_log-service-datasource_darwin_arm64 pkg/* 4 | linux: 5 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/gpx_log-service-datasource_linux_amd64 pkg/* 6 | CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o dist/gpx_log-service-datasource_linux_arm pkg/* 7 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o dist/gpx_log-service-datasource_linux_arm64 pkg/* 8 | windows: 9 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dist/gpx_log-service-datasource_windows_amd64.exe pkg/* 10 | clean: 11 | rm -f dist/gpx_log-service-datasource_* 12 | all: clean darwin linux windows 13 | #mage build -------------------------------------------------------------------------------- /old_README.md: -------------------------------------------------------------------------------- 1 | ## Aliyun log service Datasource 2 | 3 | 4 | More documentation about datasource plugins can be found in the [Docs](https://github.com/grafana/grafana/blob/master/docs/sources/plugins/developing/datasources.md). 5 | 6 | 7 | [**中文文档**](README_CN.md) 8 | 9 | 10 | ## Install 11 | 12 | 13 | Clone this project into grafana plugin directory , then restart grafana. 14 | 15 | In mac the plugin directory is /usr/local/var/lib/grafana/plugins. 16 | 17 | After install the plugin ,restart grafana 18 | 19 | ``` 20 | brew services start grafana 21 | ``` 22 | 23 | ## Add datasource 24 | 25 | In datasource management panel, add a datasource with the type "LogService". 26 | 27 | In Http settings, set Url = http://${log\_service\_endpoint} . e.g. Your projectName is accesslog in qingdao region, then the url is http://cn-qingdao.log.aliyuncs.com. 28 | 29 | Access : select `Server(Default)` 30 | 31 | log service details: 32 | 33 | set Project and logstore 34 | 35 | AccessId and AccessKey : it is better to use a sub user accessId and accessKey. 36 | 37 | To ensure data security, AK is saved and cleared without echo 38 | 39 | 40 | ## Add dashboard 41 | 42 | 43 | Add a panel, in the datasource option, choose the log service datasource that is just created. 44 | 45 | In the query : insert your query , e.g. 46 | 47 | ``` 48 | *|select count(1) as c,count(1)/2 as c1, __time__- __time__%60 as t group by t limit 10000 49 | ``` 50 | 51 | The X column ,insert t (**Second timestamp**) 52 | 53 | The Y column , insert c,c1 (**Multiple columns are separated by commas**) 54 | 55 | Save the dashboard 56 | 57 | ## Usage 58 | 59 | ### Variables 60 | 61 | In the top right corner of the dashboard panel, click dashboard Settings and select Variables. 62 | 63 | Reference variables `$VariableName` 64 | 65 | ### Flow graph 66 | 67 | The X-axis is set to the time column 68 | 69 | The Y-axis is set to the format `col1#:#col2`, where col1 is the aggregate column and col2 is the other columns 70 | 71 | The Query sample is set to 72 | ``` 73 | * | select to_unixtime(time) as time,status,count from (select time_series(__time__, '1m', '%Y-%m-%d %H:%i', '0') as time,status,count(*) as count from log group by status,time order by time limit 10000) 74 | ``` 75 | 76 | ![](https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/master/img/demo1.png) 77 | 78 | ### Pie 79 | 80 | The X-axis is set to `pie` 81 | 82 | The Y-axis is set to categories and numeric columns (example `method,pv`) 83 | 84 | The Query sample is set to 85 | ``` 86 | $hostname | select count(1) as pv ,method group by method 87 | ``` 88 | 89 | ![](https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/master/img/demo2.png) 90 | 91 | ### Table 92 | 93 | The X-axis is set to `table` or null 94 | 95 | The Y-axis is set to columns 96 | 97 | ### World map penel 98 | 99 | The X-axis is set to `map` 100 | 101 | The Y-axis is set to `country,geo,pv` 102 | 103 | The Query sample is set to 104 | ``` 105 | * | select count(1) as pv ,geohash(ip_to_geo(arbitrary(remote_addr))) as geo,ip_to_country(remote_addr) as country from log group by country having geo <>'' limit 1000 106 | ``` 107 | 108 | Location Data : `geohash` 109 | 110 | Location Name Field : `country` 111 | 112 | Geo_point/Geohash Field :" `geo` 113 | 114 | Metric Field : `pv` 115 | 116 | The query: 117 | 118 | ![](http://logdemo.oss-cn-beijing.aliyuncs.com/worldmap1.png) 119 | 120 | Parameter Settings: 121 | 122 | ![](http://logdemo.oss-cn-beijing.aliyuncs.com/worldmap2.png) 123 | 124 | ### Alert 125 | 126 | #### Mode of notification 127 | 128 | In the alert notification panel, select New channel to add 129 | 130 | #### Alert 131 | 132 | **Attention** :Dashboard alert only, not plug-in alert 133 | 134 | A sample of: 135 | 136 | ![](https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/master/img/demo3.png) 137 | 138 | Add the alert panel: 139 | 140 | ![](https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/master/img/demo4.png) 141 | 142 | - The red line on the chart represents the set threshold. Click on the right side and drag it up and down. 143 | - Evaluate every `1m` for `5m`, Is the result calculated every minute, and the threshold is exceeded for five consecutive minutes. 144 | - After setting for, if the state exceeds the threshold value and changes from Ok to Pending, the alarm will not be triggered. After continuously exceeding the threshold value for a period of time, the alarm will be sent. If the state changes from Pending to Alerting, the alarm will only be notified once. 145 | - WHEN `avg ()` OF `query (B, 5m, now)` IS ABOVE `89`, That means line B has an average of more than 89 alarms in the last five minutes. 146 | - Add notification mode and notification information under Notifications. 147 | 148 | 149 | ## grafana 7.0 150 | 151 | [Backend plugins: Unsigned external plugins should not be loaded by default #24027](https://github.com/grafana/grafana/issues/24027) 152 | 153 | 154 | ## Contributors 155 | 156 | [@WPH95](https://github.com/WPH95) made a great contribution to this project. 157 | 158 | Thanks for the excellent work by [@WPH95](https://github.com/WPH95). 159 | -------------------------------------------------------------------------------- /old_README_CN.md: -------------------------------------------------------------------------------- 1 | ## 阿里云日志服务数据源 2 | 3 | ## 编译 4 | 5 | ``` 6 | yarn build 7 | ``` 8 | 9 | ## 安装 10 | 11 | 依赖 `Grafana 8.0` 及以上版本 , `Grafana 8.0` 以下请使用 [1.0版本](https://github.com/aliyun/aliyun-log-grafana-datasource-plugin/releases/tag/1.0) 12 | 13 | 下载本插件到grafana插件目录下 , 然后重启grafana 14 | 15 | 在 mac 插件目录是 /usr/local/var/lib/grafana/plugins 16 | 17 | 重启命令为 18 | 19 | ``` 20 | brew services restart grafana 21 | ``` 22 | 23 | ## 添加数据源 24 | 25 | 在数据源管理面板, 添加 `LogService` 数据源 26 | 27 | 在 settings 面板, 设置 Url 为您日志服务 project 的 endpoint ( endpoint 在 project 的概览页可以看到). 28 | 29 | 例如你的 project 在 qingdao region, Url 可以填 `http://cn-qingdao.log.aliyuncs.com` 30 | 31 | Access 设置为 `Server(Default)` 32 | 33 | 34 | 设置 Project 和 logstore 35 | 36 | 设置 AccessId 和 AccessKeySecret , 最好配置为子账号的AK 37 | 38 | 为保证数据安全 , AK保存后清空 , 且不会回显 39 | 40 | 41 | ## 添加仪表盘 42 | 43 | 44 | 添加一个面板, 在 datasource 选项, 选择刚创建的日志服务数据源. 45 | 46 | 在 query 输入查询语句, 查询语法与日志服务控制台相同. 47 | 48 | ``` 49 | *|select count(1) as c,count(1)/2 as c1, __time__- __time__%60 as t group by t limit 10000 50 | ``` 51 | 52 | X轴设置为`t` (**秒级时间戳**) 53 | 54 | Y轴设置为`c,c1` (**多列用逗号分隔**) 55 | 56 | 保存仪表盘 57 | 58 | ## 使用 59 | 60 | ### 设置变量 61 | 62 | 在 dashboard 面板右上角点击 Dashboard settings, 选择 Variables 63 | 64 | 引用变量 `$VariableName` 65 | 66 | 变量查询语句中有双引号,需要转义成```\"``` 67 | 68 | Query 类型变量 69 | 70 | 如果开启 Multi-value 可以设置多选查询 71 | 72 | **注:多选实现为用OR连接,如果语句有多个变量AND,变量两边需要加括号($VariableName),否则OR和AND会混一起** 73 | 74 | 如果 Name 和 Label 一致 可以按字段索引查询 75 | 76 | ### 设置Logs 77 | 78 | 每页最多展示100条 79 | 80 | ### 设置单值图 81 | 82 | 选择Stat 83 | 84 | X轴 设置为`stat` 85 | 86 | Y轴 设置为查询结果的列名 87 | 88 | ### 设置流图 89 | 90 | X轴 设置为时间列 91 | 92 | Y轴 设置为 `col1#:#col2` 这种格式, 其中 col1 为 聚合列, col2 为其他列 93 | 94 | Query 设置样例为 95 | ``` 96 | * | select to_unixtime(time) as time,status,count from (select time_series(__time__, '1m', '%Y-%m-%d %H:%i', '0') as time,status,count(*) as count from log group by status,time order by time limit 10000) 97 | ``` 98 | 99 | ![](https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/master/img/demo1.png) 100 | 101 | ### 设置饼图 102 | 103 | X轴 设置为`pie` 104 | 105 | Y轴 设置为类别和数字列 (样例为`method,pv`) 106 | 107 | Query 设置样例为 108 | ``` 109 | $hostname | select count(1) as pv ,method group by method 110 | ``` 111 | 112 | ![](https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/master/img/demo2.png) 113 | 114 | ### 设置表格 115 | 116 | X轴 设置为`table` 或空 117 | 118 | Y轴 设置为列 119 | 120 | ### 设置柱形图 121 | 122 | 选择 `Bar charts` 123 | 124 | X轴 设置为`bar` 125 | 126 | Y轴 第一个值为分组列,后面的值为数字列 127 | 128 | ![](https://test-lichao.oss-cn-hangzhou.aliyuncs.com/pic/bar.jpg) 129 | 130 | ### 设置地图 131 | 132 | X轴 设置为`map` 133 | 134 | Y轴 设置为 `country,geo,pv` 135 | 136 | Query 设置样例为 137 | ``` 138 | * | select count(1) as pv ,geohash(ip_to_geo(arbitrary(remote_addr))) as geo,ip_to_country(remote_addr) as country from log group by country having geo <>'' limit 1000 139 | ``` 140 | 141 | Location Data 设置为 `geohash` 142 | 143 | Location Name Field 设置为 `country` 144 | 145 | geo_point/geohash Field 设置为 `geo` 146 | 147 | Metric Field 设置为 `pv` 148 | 149 | 查询语句: 150 | 151 | ![](http://logdemo.oss-cn-beijing.aliyuncs.com/worldmap1.png) 152 | 153 | 参数设置: 154 | 155 | ![](http://logdemo.oss-cn-beijing.aliyuncs.com/worldmap2.png) 156 | 157 | ### 设置Trace 158 | 159 | [**Trace数据格式**](https://help.aliyun.com/document_detail/208891.html) 160 | 161 | 在 Explore 面板 162 | 163 | X轴 设置为`trace` 164 | 165 | ![](https://test-lichao.oss-cn-hangzhou.aliyuncs.com/pic/trace.jpg) 166 | 167 | ### 设置告警 168 | 169 | #### 通知方式 170 | 171 | 在告警通知方式面板, 选择 New channel 添加 172 | 173 | **注意** :选择dingding告警, 在钉钉机器人的安全设置里选自定义关键词, 添加 `Alerting` 174 | 175 | #### 添加告警 176 | 177 | **注意** :只支持dashboard告警, 不支持插件告警 178 | 179 | 样例如下: 180 | 181 | ![](https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/master/img/demo3.png) 182 | 183 | 添加告警面板: 184 | 185 | ![](https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/master/img/demo4.png) 186 | 187 | - 其中图表上红线代表设置的阈值, 点击右侧可以上下拖动 188 | - Evaluate every `1m` for `5m`, 代表计算每分钟的结果, 连续五分钟超过阈值告警 189 | - 设置for后, 如果超过阈值状态由Ok转为Pending, 不会触发告警, 连续超过阈值一段时候后发送告警, 状态由Pending转为Alerting, 告警只会通知一次 190 | - WHEN `avg ()` OF `query (B, 5m, now)` IS ABOVE `89`, 代表线条B最近五分钟的均值超过89告警 191 | - 在Notifications下添加通知方式及通知信息 192 | 193 | 194 | ## 常见问题 195 | 196 | 197 | ### 错误诊断 198 | 199 | 查看grafana 日志 200 | 201 | 在 mac 日志目录是 /usr/local/var/log/grafana 202 | 203 | 在 linux 日志目录是 /var/log/grafana 204 | 205 | - aliyun-log-plugin_linux_amd64: permission denied , 需要授予插件目录下dist/aliyun-log-plugin_linux_amd64执行权限 206 | 207 | 208 | ### grafana 7.0 209 | 210 | 参考 [Backend plugins: Unsigned external plugins should not be loaded by default #24027](https://github.com/grafana/grafana/issues/24027) 211 | 212 | 修改grafana配置文件 213 | 214 | 在mac上一般为 `/usr/local/etc/grafana/grafana.ini` 215 | 216 | 在linux上一般为 `/etc/grafana/grafana.ini` 217 | 218 | 在`[plugins]`标签下设置参数 219 | 220 | `allow_loading_unsigned_plugins = aliyun-log-service-datasource` 221 | 222 | 223 | ### 添加source变量 224 | 225 | `* | select distinct __source__ as source` 226 | 227 | __source__字段默认会被过滤掉,需要用as起别名 228 | 229 | ### provision 配置 230 | 231 | ```yml 232 | apiVersion: 1 233 | 234 | datasources: 235 | - name: LogService 236 | type: aliyun-log-service-datasource 237 | url: http://cn-hangzhou.log.aliyuncs.com 238 | jsonData: 239 | project: xxxxx 240 | logstore: xxxxx 241 | secureJsonData: 242 | accessKeyId: xxxx 243 | accessKeySecret: xxxxx 244 | editable: true 245 | ``` 246 | 247 | 248 | ### 钉钉群 249 | 250 | 251 | 252 | 253 | 254 | 255 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aliyun-log-service-datasource", 3 | "version": "2.38", 4 | "description": "Aliyun log service datasource (backend version)", 5 | "scripts": { 6 | "build": "grafana-toolkit plugin:build && mage", 7 | "build-front": "grafana-toolkit plugin:build", 8 | "build-back": "mage", 9 | "dev-back": "mage; launchctl stop com.aliyun.sls.autoGrafana; launchctl start com.aliyun.sls.autoGrafana", 10 | "test": "grafana-toolkit plugin:test", 11 | "dev": "grafana-toolkit plugin:dev", 12 | "watch": "grafana-toolkit plugin:dev --watch", 13 | "sign": "grafana-toolkit plugin:sign", 14 | "start": "yarn watch" 15 | }, 16 | "author": "aliyun-log", 17 | "license": "Apache-2.0", 18 | "devDependencies": { 19 | "@grafana/data": "9.1.2", 20 | "@grafana/runtime": "9.1.2", 21 | "@grafana/toolkit": "9.1.2", 22 | "@grafana/experimental": "1.8.0", 23 | "@grafana/ui": "9.1.2", 24 | "@types/lodash": "latest", 25 | "@types/uuid": "9.0.1" 26 | }, 27 | "resolutions": { 28 | "rxjs": "6.6.3" 29 | }, 30 | "engines": { 31 | "node": ">=14" 32 | }, 33 | "dependencies": { 34 | "js-base64": "^3.7.5", 35 | "urijs": "^1.19.11" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | _ "net/http/pprof" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | 11 | "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" 12 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 13 | ) 14 | 15 | func main() { 16 | // Start listening to requests sent from Grafana. This call is blocking so 17 | // it won't finish until Grafana shuts down the process or the plugin choose 18 | // to exit by itself using os.Exit. Manage automatically manages life cycle 19 | // of datasource instances. It accepts datasource instance factory as first 20 | // argument. This factory will be automatically called on incoming request 21 | // from Grafana to create different instances of SampleDatasource (per datasource 22 | // ID). When datasource configuration changed Dispose method will be called and 23 | // new datasource instance created using NewSampleDatasource factory. 24 | 25 | loadConfig() 26 | // if err := datasource.Manage("aliyun-log-backend-datasource", NewSLSDatasource, datasource.ManageOpts{}); err != nil { 27 | // log.DefaultLogger.Error(err.Error()) 28 | // os.Exit(1) 29 | // } 30 | err := datasource.Serve(NewSLSDatasource()) 31 | grafanaVersion := os.Getenv("GF_VERSION") 32 | log.DefaultLogger.Info("GF_VERSION", grafanaVersion) 33 | 34 | if err != nil { 35 | log.DefaultLogger.Error(err.Error()) 36 | os.Exit(1) 37 | } 38 | } 39 | 40 | var maxPointsLimit = 6000000 41 | var compatible = false 42 | 43 | func loadConfig() { 44 | ex, err := os.Executable() 45 | if err != nil { 46 | log.DefaultLogger.Info("", err) 47 | return 48 | } 49 | exPath := filepath.Dir(ex) 50 | b, err := ioutil.ReadFile(exPath + "/plugin.json") 51 | if err != nil { 52 | log.DefaultLogger.Info("", err) 53 | return 54 | } 55 | m := map[string]interface{}{} 56 | err = json.Unmarshal(b, &m) 57 | if err != nil { 58 | log.DefaultLogger.Info("", err) 59 | return 60 | } 61 | maxPointsI := m["flow_chart_max_points"] 62 | if maxPointsI == nil { 63 | return 64 | } 65 | if _, ok := maxPointsI.(string); !ok { 66 | return 67 | } 68 | parseInt, err := strconv.ParseInt(maxPointsI.(string), 0, 0) 69 | if err != nil { 70 | log.DefaultLogger.Info("", err) 71 | return 72 | } 73 | maxPointsLimit = int(parseInt) 74 | 75 | compatibleI := m["legacy_compatible"] 76 | if compatibleI == nil { 77 | return 78 | } 79 | if _, ok := compatibleI.(bool); ok { 80 | compatible = compatibleI.(bool) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/models.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | ) 9 | 10 | type Header struct { 11 | Name string `json:"name"` 12 | Value string `json:"value"` 13 | } 14 | 15 | type LogSource struct { 16 | Endpoint string 17 | Project string `json:"project"` 18 | LogStore string `json:"logstore"` 19 | RoleArn string `json:"roleArn"` 20 | Region string 21 | AccessKeyId string 22 | AccessKeySecret string 23 | Headers []Header `json:"headers"` 24 | } 25 | 26 | type QueryInfo struct { 27 | Type string `json:"type"` 28 | QueryMode string `json:"mode"` 29 | Query string `json:"query"` 30 | Xcol string `json:"xcol"` 31 | Ycol string `json:"ycol"` 32 | LogsPerPage int64 `json:"logsPerPage"` 33 | CurrentPage int64 `json:"currentPage"` 34 | LogStore string `json:"logStore"` 35 | LegendFormat string `json:"legendFormat"` 36 | QueryType string `json:"queryType"` 37 | Step string `json:"step"` 38 | IntervalMs int64 `json:"intervalMs"` 39 | TotalLogs int64 `json:"totalLogs"` 40 | PowerSql bool `json:"powerSql"` 41 | } 42 | 43 | type Result struct { 44 | refId string 45 | dataResponse backend.DataResponse 46 | } 47 | 48 | // Contents {"keys":["c","c1","t"],"terms":[["*",""]],"limited":"100"} 49 | type Contents struct { 50 | Keys []string `json:"keys"` 51 | Terms [][]string `json:"terms"` 52 | Limited string `json:"limited"` 53 | } 54 | 55 | type ResultItem struct { 56 | Metric map[string]string `json:"metric"` 57 | Values [][]interface{} `json:"values"` 58 | Value []interface{} `json:"value"` 59 | } 60 | type MetricData struct { 61 | ResultType string `json:"resultType"` 62 | Result []ResultItem `json:"result"` 63 | } 64 | type MetricLogs struct { 65 | Status string `json:"status"` 66 | Data MetricData `json:"data"` 67 | } 68 | 69 | func LoadSettings(ctx backend.PluginContext) (*LogSource, error) { 70 | model := &LogSource{} 71 | 72 | settings := ctx.DataSourceInstanceSettings 73 | err := json.Unmarshal(settings.JSONData, &model) 74 | if err != nil { 75 | return nil, fmt.Errorf("error reading settings: %s", err.Error()) 76 | } 77 | model.Endpoint = settings.URL 78 | model.AccessKeyId = settings.DecryptedSecureJSONData["accessKeyId"] 79 | model.AccessKeySecret = settings.DecryptedSecureJSONData["accessKeySecret"] 80 | 81 | return model, nil 82 | } 83 | -------------------------------------------------------------------------------- /pkg/ram.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/aliyun/alibaba-cloud-sdk-go/sdk" 8 | "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" 9 | ram "github.com/aliyun/alibaba-cloud-sdk-go/services/ram" 10 | ) 11 | 12 | var POLICY_LEN_ERROR = "policy length should be 1. Role policy should ONLY have AliyunLogReadOnlyAccess." 13 | var RAM_NO_PERMISSION_ERROR = "configured user should have AliyunRAMReadOnlyAccess." 14 | var POLYCY_NOT_MATCH_ERROR = "role policy should only have AliyunLogReadOnlyAccess." 15 | 16 | func roleCheck(ak string, sk string, roleName string) ([]ram.Policy, error) { 17 | config := sdk.NewConfig() 18 | credential := credentials.NewAccessKeyCredential(ak, sk) 19 | // log.DefaultLogger.Info("roleName", roleName) 20 | client, err := ram.NewClientWithOptions("cn-hangzhou", config, credential) 21 | if err != nil { 22 | return nil, err 23 | } 24 | request := ram.CreateListPoliciesForRoleRequest() 25 | request.Scheme = "https" 26 | request.RoleName = roleName 27 | 28 | response, err := client.ListPoliciesForRole(request) 29 | if err != nil { 30 | s := err.Error() 31 | if strings.Contains(s, "NoPermission") { 32 | return nil, errors.New(RAM_NO_PERMISSION_ERROR) 33 | } 34 | return nil, err 35 | } 36 | policyList := response.Policies.Policy 37 | len := len(policyList) 38 | if len != 1 { 39 | return nil, errors.New(POLICY_LEN_ERROR) 40 | } 41 | if policyList[0].PolicyName != "AliyunLogReadOnlyAccess" { 42 | return nil, errors.New(POLYCY_NOT_MATCH_ERROR) 43 | } 44 | return policyList, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/resource.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "regexp" 9 | "strings" 10 | 11 | sls "github.com/aliyun/aliyun-log-go-sdk" 12 | "gitlab.alibaba-inc.com/rapt/go-security-utils/network" 13 | 14 | "github.com/grafana/grafana-plugin-sdk-go/backend" 15 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 16 | "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" 17 | ) 18 | 19 | func newResourceHandler(ds *SlsDatasource) backend.CallResourceHandler { 20 | mux := http.NewServeMux() 21 | 22 | // register route 23 | mux.HandleFunc("/api/gotoSLS", ds.gotoSLS) 24 | mux.HandleFunc("/api/version", ds.serveVersion) 25 | mux.HandleFunc("/api/getLogstoreList", ds.getLogstoreList) 26 | 27 | return httpadapter.New(mux) 28 | } 29 | 30 | func (ds *SlsDatasource) serveVersion(w http.ResponseWriter, r *http.Request) { 31 | // Handle query request... 32 | } 33 | 34 | type ListLogstoresData struct { 35 | Project string 36 | TelemetryType string 37 | } 38 | 39 | func (ds *SlsDatasource) getLogstoreList(w http.ResponseWriter, r *http.Request) { 40 | response := map[string]interface{}{ 41 | "data": []map[string]interface{}{}, // 定义 data 为一个任意类型的对象数组 42 | "res": nil, 43 | "message": "", 44 | } 45 | 46 | config, err := LoadSettings(httpadapter.PluginConfigFromContext(r.Context())) 47 | if err != nil { 48 | response["message"] = err.Error() 49 | http.Error(w, err.Error(), http.StatusBadRequest) 50 | return 51 | } 52 | 53 | provider := sls.NewStaticCredentialsProvider(config.AccessKeyId, config.AccessKeySecret, "") 54 | client := sls.CreateNormalInterfaceV2(config.Endpoint, provider) 55 | client.SetUserAgent("grafana-go") 56 | 57 | if config.Region != "" { 58 | client.SetAuthVersion(sls.AuthV4) 59 | client.SetRegion(config.Region) 60 | } 61 | 62 | body, err := ioutil.ReadAll(r.Body) 63 | if err != nil { 64 | response["message"] = err.Error() 65 | http.Error(w, err.Error(), http.StatusBadRequest) 66 | return 67 | } 68 | 69 | // 解析request JSON 数据 70 | var data ListLogstoresData 71 | if err := json.Unmarshal(body, &data); err != nil { 72 | response["message"] = err.Error() 73 | http.Error(w, err.Error(), http.StatusBadRequest) 74 | return 75 | } 76 | 77 | log.DefaultLogger.Debug("getBODY", "body", body, "bodyData", data) 78 | 79 | project, err := client.GetProject(data.Project) 80 | if err != nil { 81 | response["message"] = err.Error() 82 | http.Error(w, err.Error(), http.StatusBadRequest) 83 | return 84 | } 85 | 86 | // 拿当前 Project 的信息 87 | list, err := project.ListLogStoreV2(0, 500, data.TelemetryType) 88 | if err != nil { 89 | response["message"] = err.Error() 90 | http.Error(w, err.Error(), http.StatusBadRequest) 91 | return 92 | } 93 | 94 | response["data"] = list 95 | response["res"] = list 96 | 97 | w.Header().Set("Content-Type", "application/json") 98 | w.WriteHeader(http.StatusOK) 99 | json.NewEncoder(w).Encode(response) 100 | log.DefaultLogger.Debug("get logstore success.") 101 | } 102 | 103 | type Data struct { 104 | Encoding string `json:"encoding"` 105 | Logstore string `json:"logstore"` 106 | Type string `json:"type"` 107 | } 108 | 109 | func (ds *SlsDatasource) gotoSLS(w http.ResponseWriter, r *http.Request) { 110 | 111 | response := map[string]interface{}{ 112 | "message": "", 113 | "err": "", 114 | "url": "", 115 | // "policy": "", 116 | } 117 | 118 | config, err := LoadSettings(httpadapter.PluginConfigFromContext(r.Context())) 119 | if err != nil { 120 | http.Error(w, err.Error(), http.StatusBadRequest) 121 | return 122 | } 123 | 124 | ak := config.AccessKeyId 125 | sk := config.AccessKeySecret 126 | arn := config.RoleArn 127 | prj := config.Project 128 | logstore := config.LogStore 129 | 130 | body, err := ioutil.ReadAll(r.Body) 131 | if err != nil { 132 | http.Error(w, err.Error(), http.StatusBadRequest) 133 | return 134 | } 135 | 136 | // 解析request JSON 数据 137 | var data Data 138 | if err := json.Unmarshal(body, &data); err != nil { 139 | http.Error(w, err.Error(), http.StatusBadRequest) 140 | return 141 | } 142 | 143 | logstoreType := "/logsearch/" 144 | 145 | if data.Type == "metricsql" || data.Type == "metricstore" { 146 | logstoreType = "/metric/" 147 | } 148 | 149 | if data.Logstore != "" { 150 | logstore = data.Logstore 151 | } 152 | 153 | pattern := `^acs:ram::\d+:role\/[^\/]+$` 154 | regex, err := regexp.Compile(pattern) 155 | if err != nil { 156 | return 157 | } 158 | 159 | normalJump := false 160 | 161 | if len(arn) == 0 { 162 | normalJump = true 163 | } else { 164 | if !regex.MatchString(arn) { 165 | response["err"] = "regexCheckError" 166 | response["message"] = "roleArn 不符合格式,请检查。" 167 | normalJump = true 168 | } 169 | } 170 | 171 | if !normalJump { 172 | roleName := strings.Split(arn, "/")[1] 173 | _, err2 := roleCheck(ak, sk, roleName) 174 | if err2 != nil { 175 | response["err"] = "roleCheckError" 176 | response["message"] = err2.Error() 177 | // http.Error(w, err2.Error(), http.StatusBadRequest) 178 | // return 179 | normalJump = true 180 | } 181 | // response["policy"] = p 182 | } 183 | 184 | if !normalJump { 185 | client := NewClient(ak, sk, arn, "default") 186 | stsResp, err := client.AssumeRole(900) 187 | if err != nil { 188 | http.Error(w, err.Error(), http.StatusInternalServerError) 189 | log.DefaultLogger.Error(err.Error()) 190 | // response["err"] = err.Error() 191 | // response["message"] = err.Error() 192 | // w.Header().Set("Content-Type", "application/json") 193 | // w.WriteHeader(http.StatusInternalServerError) 194 | // json.NewEncoder(w).Encode(response) 195 | return 196 | } 197 | id := stsResp.Credentials.AccessKeyId 198 | secret := stsResp.Credentials.AccessKeySecret 199 | token := stsResp.Credentials.SecurityToken 200 | 201 | // 使用STS Token换取控制台Signin Token 202 | SigninResp, err := getSigninToken(id, secret, token) 203 | if err != nil { 204 | panic(err) 205 | } 206 | signinToken := SigninResp.SigninToken 207 | 208 | // 生成登录链接 209 | loginUrl := "http://www.aliyun.com" 210 | // destination := "http://sls4service.console.aliyun.com" 211 | destination := "http://sls4service.console.aliyun.com/lognext/project/" + prj + logstoreType + logstore + "?isShare=true&hideTopbar=true&hideSidebar=true&ignoreTabLocalStorage=true&" + data.Encoding 212 | url, err := genSigninUrl(signinToken, loginUrl, destination) 213 | if err != nil { 214 | http.Error(w, err.Error(), http.StatusInternalServerError) 215 | log.DefaultLogger.Error(err.Error()) 216 | return 217 | } 218 | 219 | response["url"] = url 220 | w.Header().Set("Content-Type", "application/json") 221 | w.WriteHeader(http.StatusOK) 222 | json.NewEncoder(w).Encode(response) 223 | log.DefaultLogger.Debug("Goto SLS with STS success.", url) 224 | return 225 | } 226 | url := "https://sls.console.aliyun.com/lognext/project/" + prj + logstoreType + logstore + "?" + data.Encoding 227 | response["url"] = url 228 | w.Header().Set("Content-Type", "application/json") 229 | w.WriteHeader(http.StatusOK) 230 | json.NewEncoder(w).Encode(response) 231 | log.DefaultLogger.Debug("Goto SLS with Normal jump success.", url) 232 | } 233 | 234 | func getSigninToken(id string, secret string, token string) (*SigninResponse, error) { 235 | urlStr := "http://signin.aliyun.com/federation?Action=GetSigninToken" 236 | urlStr += "&AccessKeyId=" + id 237 | urlStr += "&AccessKeySecret=" + secret 238 | urlStr += "&SecurityToken=" + url.QueryEscape(token) 239 | urlStr += "&TicketType=mini" 240 | 241 | transport := http.DefaultTransport.(*http.Transport).Clone() 242 | transport.DialContext = network.DefaultNetworkFilter.FilterHttpDialContext(transport.DialContext) 243 | client := &http.Client{ 244 | Transport: transport, 245 | } 246 | 247 | res, err := client.Get(urlStr) 248 | if err != nil { 249 | return nil, err 250 | } 251 | body, err := ioutil.ReadAll(res.Body) 252 | if err != nil { 253 | return nil, err 254 | } 255 | // fmt.Println("SigninToken json:", string(body)) 256 | resp := SigninResponse{} 257 | err = json.Unmarshal(body, &resp) 258 | if err != nil { 259 | return nil, err 260 | } 261 | return &resp, nil 262 | } 263 | 264 | func genSigninUrl(signinToken string, loginUrl string, destination string) (string, error) { 265 | urlStr := "http://signin.aliyun.com/federation?Action=Login" 266 | urlStr += "&LoginUrl=" + url.QueryEscape(loginUrl) 267 | urlStr += "&Destination=" + url.QueryEscape(destination) 268 | urlStr += "&SigninToken=" + url.QueryEscape(signinToken) 269 | client := &http.Client{ 270 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 271 | return http.ErrUseLastResponse 272 | }, 273 | } 274 | res, err := client.Get(urlStr) 275 | if err != nil { 276 | return "", err 277 | } 278 | location, err := res.Location() 279 | if err != nil { 280 | return "", err 281 | } 282 | locationUrl := location.String() 283 | return locationUrl, nil 284 | } 285 | 286 | type SigninResponse struct { 287 | SigninToken string 288 | } 289 | -------------------------------------------------------------------------------- /pkg/sts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "crypto/tls" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "strconv" 14 | "time" 15 | 16 | uuid "github.com/satori/go.uuid" 17 | ) 18 | 19 | // Client sts client 20 | type Client struct { 21 | AccessKeyId string 22 | AccessKeySecret string 23 | RoleArn string 24 | SessionName string 25 | } 26 | 27 | // ServiceError sts service error 28 | type ServiceError struct { 29 | Code string 30 | Message string 31 | RequestId string 32 | HostId string 33 | RawMessage string 34 | StatusCode int 35 | } 36 | 37 | // Credentials the credentials obtained by AssumedRole, 38 | // used for the peration of Alibaba Cloud service. 39 | type Credentials struct { 40 | AccessKeyId string 41 | AccessKeySecret string 42 | Expiration time.Time 43 | SecurityToken string 44 | } 45 | 46 | // AssumedRoleUser the user to AssumedRole 47 | type AssumedRoleUser struct { 48 | Arn string 49 | AssumedRoleId string 50 | } 51 | 52 | // Response the response of AssumeRole 53 | type Response struct { 54 | Credentials Credentials 55 | AssumedRoleUser AssumedRoleUser 56 | RequestId string 57 | } 58 | 59 | // Error implement interface error 60 | func (e *ServiceError) Error() string { 61 | return fmt.Sprintf("oss: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestId=%s", 62 | e.StatusCode, e.Code, e.Message, e.RequestId) 63 | } 64 | 65 | // NewClient New STS Client 66 | func NewClient(accessKeyId, accessKeySecret, roleArn, sessionName string) *Client { 67 | return &Client{ 68 | AccessKeyId: accessKeyId, 69 | AccessKeySecret: accessKeySecret, 70 | RoleArn: roleArn, 71 | SessionName: sessionName, 72 | } 73 | } 74 | 75 | const ( 76 | // StsSignVersion sts sign version 77 | StsSignVersion = "1.0" 78 | // StsAPIVersion sts api version 79 | StsAPIVersion = "2015-04-01" 80 | // StsHost sts host 81 | StsHost = "https://sts.aliyuncs.com/" 82 | // TimeFormat time fomrat 83 | TimeFormat = "2006-01-02T15:04:05Z" 84 | // RespBodyFormat respone body format 85 | RespBodyFormat = "JSON" 86 | // PercentEncode '/' 87 | PercentEncode = "%2F" 88 | // HTTPGet http get method 89 | HTTPGet = "GET" 90 | ) 91 | 92 | // AssumeRole assume role 93 | func (c *Client) AssumeRole(expiredTime uint) (*Response, error) { 94 | url, err := c.generateSignedURL(expiredTime) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | body, status, err := c.sendRequest(url) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return c.handleResponse(body, status) 105 | } 106 | 107 | // Private function 108 | func (c *Client) generateSignedURL(expiredTime uint) (string, error) { 109 | uid := uuid.NewV4() 110 | rst := uid.String() 111 | queryStr := "SignatureVersion=" + StsSignVersion 112 | queryStr += "&Format=" + RespBodyFormat 113 | queryStr += "&Timestamp=" + url.QueryEscape(time.Now().UTC().Format(TimeFormat)) 114 | queryStr += "&RoleArn=" + url.QueryEscape(c.RoleArn) 115 | queryStr += "&RoleSessionName=" + c.SessionName 116 | queryStr += "&AccessKeyId=" + c.AccessKeyId 117 | queryStr += "&SignatureMethod=HMAC-SHA1" 118 | queryStr += "&Version=" + StsAPIVersion 119 | queryStr += "&Action=AssumeRole" 120 | queryStr += "&SignatureNonce=" + rst 121 | queryStr += "&DurationSeconds=" + strconv.FormatUint((uint64)(expiredTime), 10) 122 | 123 | // Sort query string 124 | queryParams, err := url.ParseQuery(queryStr) 125 | if err != nil { 126 | return "", err 127 | } 128 | result := queryParams.Encode() 129 | 130 | strToSign := HTTPGet + "&" + PercentEncode + "&" + url.QueryEscape(result) 131 | 132 | // Generate signature 133 | hashSign := hmac.New(sha1.New, []byte(c.AccessKeySecret+"&")) 134 | hashSign.Write([]byte(strToSign)) 135 | signature := base64.StdEncoding.EncodeToString(hashSign.Sum(nil)) 136 | 137 | // Build url 138 | assumeURL := StsHost + "?" + queryStr + "&Signature=" + url.QueryEscape(signature) 139 | 140 | return assumeURL, nil 141 | } 142 | 143 | func (c *Client) sendRequest(url string) ([]byte, int, error) { 144 | tr := &http.Transport{ 145 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 146 | } 147 | client := &http.Client{Transport: tr} 148 | 149 | resp, err := client.Get(url) 150 | if err != nil { 151 | return nil, -1, err 152 | } 153 | defer resp.Body.Close() 154 | 155 | body, err := ioutil.ReadAll(resp.Body) 156 | return body, resp.StatusCode, err 157 | } 158 | 159 | func (c *Client) handleResponse(responseBody []byte, statusCode int) (*Response, error) { 160 | if statusCode != http.StatusOK { 161 | se := ServiceError{StatusCode: statusCode, RawMessage: string(responseBody)} 162 | err := json.Unmarshal(responseBody, &se) 163 | if err != nil { 164 | return nil, err 165 | } 166 | return nil, &se 167 | } 168 | 169 | resp := Response{} 170 | err := json.Unmarshal(responseBody, &resp) 171 | if err != nil { 172 | return nil, err 173 | } 174 | return &resp, nil 175 | } 176 | -------------------------------------------------------------------------------- /src/ConfigEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, PureComponent } from 'react'; 2 | import { Input, InlineField } from '@grafana/ui'; 3 | import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; 4 | import { HeaderWithValue, SLSDataSourceOptions, SLSSecureJsonData } from './types'; 5 | import CustomHeaders from './custom-header/custom-headers'; 6 | import { ConfigSection } from './components/configSection'; 7 | import { DataSourceDescription } from './components/description'; 8 | import { SecretInput } from './components/QueryEditor/SecretInput'; 9 | 10 | interface Props extends DataSourcePluginOptionsEditorProps {} 11 | 12 | interface State {} 13 | 14 | export class SLSConfigEditor extends PureComponent { 15 | onEndpointChange = (event: ChangeEvent) => { 16 | const { onOptionsChange, options } = this.props; 17 | options.url = event.target.value; 18 | onOptionsChange({ ...options }); 19 | }; 20 | 21 | onProjectChange = (event: ChangeEvent) => { 22 | const { onOptionsChange, options } = this.props; 23 | const jsonData = { 24 | ...options.jsonData, 25 | project: event.target.value, 26 | }; 27 | onOptionsChange({ ...options, jsonData }); 28 | }; 29 | 30 | onLogStoreChange = (event: ChangeEvent) => { 31 | const { onOptionsChange, options } = this.props; 32 | const jsonData = { 33 | ...options.jsonData, 34 | logstore: event.target.value, 35 | }; 36 | onOptionsChange({ ...options, jsonData }); 37 | }; 38 | 39 | onRegionChange = (event: ChangeEvent) => { 40 | const { onOptionsChange, options } = this.props; 41 | const jsonData = { 42 | ...options.jsonData, 43 | region: event.target.value, 44 | }; 45 | onOptionsChange({ ...options, jsonData }); 46 | }; 47 | onRoleArnChange = (event: ChangeEvent) => { 48 | const { onOptionsChange, options } = this.props; 49 | const jsonData = { 50 | ...options.jsonData, 51 | roleArn: event.target.value, 52 | }; 53 | onOptionsChange({ ...options, jsonData }); 54 | }; 55 | 56 | // Secure field (only sent to the backend) 57 | onAKIDChange = (event: ChangeEvent) => { 58 | const { onOptionsChange, options } = this.props; 59 | var accessKeySecret = ''; 60 | if (options.secureJsonData !== undefined) { 61 | if (options.secureJsonData.hasOwnProperty('accessKeySecret')) { 62 | // @ts-ignore 63 | accessKeySecret = options.secureJsonData['accessKeySecret']; 64 | } 65 | } 66 | onOptionsChange({ 67 | ...options, 68 | secureJsonData: { 69 | accessKeyId: event.target.value, 70 | accessKeySecret: accessKeySecret, 71 | }, 72 | }); 73 | }; 74 | 75 | onAKSecretChange = (event: ChangeEvent) => { 76 | const { onOptionsChange, options } = this.props; 77 | var accessKeyId = ''; 78 | if (options.secureJsonData !== undefined) { 79 | if (options.secureJsonData.hasOwnProperty('accessKeyId')) { 80 | // @ts-ignore 81 | accessKeyId = options.secureJsonData['accessKeyId']; 82 | } 83 | } 84 | onOptionsChange({ 85 | ...options, 86 | secureJsonData: { 87 | accessKeyId: accessKeyId, 88 | accessKeySecret: event.target.value, 89 | }, 90 | }); 91 | }; 92 | 93 | onResetAKID = () => { 94 | const { onOptionsChange, options } = this.props; 95 | onOptionsChange({ 96 | ...options, 97 | secureJsonFields: { 98 | ...options.secureJsonFields, 99 | accessKeyId: false, 100 | }, 101 | secureJsonData: { 102 | ...options.secureJsonData, 103 | accessKeyId: '', 104 | }, 105 | }); 106 | }; 107 | onResetAKSecret = () => { 108 | const { onOptionsChange, options } = this.props; 109 | onOptionsChange({ 110 | ...options, 111 | secureJsonFields: { 112 | ...options.secureJsonFields, 113 | accessKeySecret: false, 114 | }, 115 | secureJsonData: { 116 | ...options.secureJsonData, 117 | accessKeySecret: '', 118 | }, 119 | }); 120 | }; 121 | 122 | onHeadersChange = (headers: HeaderWithValue[]) => { 123 | const { onOptionsChange, options } = this.props; 124 | const jsonData = { 125 | ...options.jsonData, 126 | headers, 127 | }; 128 | onOptionsChange({ ...options, jsonData }); 129 | }; 130 | 131 | render() { 132 | const { options } = this.props; 133 | const { jsonData, secureJsonFields, url } = options; 134 | const secureJsonData = (options.secureJsonData || {}) as SLSSecureJsonData; 135 | 136 | return ( 137 | <> 138 | 142 |
143 | 144 | {/* EndPonit */} 145 | 151 | Specify a complete Endpoint URL 152 |
153 | (for example: cn-hangzhou.log.aliyuncs.com) 154 | 155 | } 156 | grow 157 | interactive 158 | > 159 | 166 |
167 | 168 | {/* Project */} 169 | 177 | 184 | 185 | 186 | {/* AccessKeyID */} 187 | 195 | 203 | 204 | 205 | {/* AccessKeySecret */} 206 | 214 | 222 | 223 | 224 | {/* Other */} 225 | 232 | Region 233 |
234 | (for example: cn-hangzhou) 235 | 236 | } 237 | grow 238 | interactive 239 | > 240 | 247 |
248 |
249 | 250 | {/* Other */} 251 |
252 | 253 | 262 | 269 | 270 | 271 | {/* roleArn */} 272 | 280 | 287 | 288 | 289 | {/* headers */} 290 | 291 | 292 | 293 | ); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/SLS-monaco-editor/MonacoQueryField.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { debounce } from 'lodash'; 3 | import { slsLanguageDefinition } from './language-definition/definition'; 4 | import React, { useRef, useEffect } from 'react'; 5 | import { useLatest } from 'react-use'; 6 | import { v4 as uuidv4 } from 'uuid'; 7 | 8 | import { GrafanaTheme2 } from '@grafana/data'; 9 | import { selectors } from '@grafana/e2e-selectors'; 10 | import { useTheme2, ReactMonacoEditor, Monaco, monacoTypes } from '@grafana/ui'; 11 | 12 | import { Props } from './MonacoQueryFieldProps'; 13 | import { getOverrideServices } from './getOverrideServices'; 14 | import { completionItemProvider, language, languageConfiguration } from './language-definition/sls'; 15 | 16 | const options: monacoTypes.editor.IStandaloneEditorConstructionOptions = { 17 | codeLens: false, 18 | contextmenu: false, 19 | fixedOverflowWidgets: true, 20 | folding: false, 21 | fontSize: 14, 22 | lineDecorationsWidth: 8, 23 | lineNumbers: 'off', 24 | minimap: { enabled: false }, 25 | overviewRulerBorder: false, 26 | overviewRulerLanes: 0, 27 | padding: { 28 | top: 4, 29 | bottom: 5, 30 | }, 31 | renderLineHighlight: 'none', 32 | scrollbar: { 33 | vertical: 'hidden', 34 | verticalScrollbarSize: 8, 35 | horizontal: 'hidden', 36 | horizontalScrollbarSize: 0, 37 | }, 38 | scrollBeyondLastLine: false, 39 | // suggest: getSuggestOptions(), 40 | suggestFontSize: 12, 41 | wordWrap: 'on', 42 | }; 43 | 44 | // single line and multiline 45 | const EDITOR_HEIGHT_OFFSET = 2; 46 | const SLS_LANG_ID = slsLanguageDefinition.id; 47 | 48 | let SLS_SETUP_STARTED = false; 49 | 50 | function ensureSLSQL(monaco: Monaco) { 51 | if (SLS_SETUP_STARTED === false) { 52 | SLS_SETUP_STARTED = true; 53 | const { aliases, extensions, mimetypes } = slsLanguageDefinition; 54 | monaco.languages.register({ id: SLS_LANG_ID, aliases, extensions, mimetypes }); 55 | monaco.languages.setMonarchTokensProvider(SLS_LANG_ID, language); 56 | monaco.languages.setLanguageConfiguration(SLS_LANG_ID, languageConfiguration); 57 | // loader().then((mod) => { 58 | // monaco.languages.setMonarchTokensProvider(SLS_LANG_ID, mod.language); 59 | // monaco.languages.setLanguageConfiguration(SLS_LANG_ID, mod.languageConfiguration); 60 | // }); 61 | } 62 | } 63 | 64 | const getStyles = (theme: GrafanaTheme2, placeholder: string) => { 65 | return { 66 | container: css` 67 | border-radius: ${theme.shape.borderRadius()}; 68 | border: 1px solid ${theme.components.input.borderColor}; 69 | `, 70 | placeholder: css` 71 | ::after { 72 | content: '${placeholder}'; 73 | font-family: ${theme.typography.fontFamilyMonospace}; 74 | opacity: 0.3; 75 | } 76 | `, 77 | }; 78 | }; 79 | 80 | const MonacoQueryField = (props: Props) => { 81 | const id = uuidv4(); 82 | 83 | const overrideServicesRef = useRef(getOverrideServices()); 84 | const containerRef = useRef(null); 85 | const { onBlur, onRunQuery, initialValue, placeholder, onChange } = props; 86 | 87 | // const lpRef = useLatest(languageProvider); 88 | // const historyRef = useLatest(history); 89 | const onRunQueryRef = useLatest(onRunQuery); 90 | const onBlurRef = useLatest(onBlur); 91 | const onChangeRef = useLatest(onChange); 92 | 93 | const autocompleteDisposeFun = useRef<(() => void) | null>(null); 94 | 95 | const theme = useTheme2(); 96 | const styles = getStyles(theme, placeholder); 97 | 98 | useEffect(() => { 99 | return () => { 100 | autocompleteDisposeFun.current?.(); 101 | }; 102 | }, []); 103 | 104 | return ( 105 |
110 | { 116 | ensureSLSQL(monaco); 117 | }} 118 | onMount={(editor, monaco) => { 119 | const isEditorFocused = editor.createContextKey('isEditorFocused' + id, false); 120 | 121 | editor.onDidBlurEditorWidget(() => { 122 | isEditorFocused.set(false); 123 | onBlurRef.current(editor.getValue()); 124 | }); 125 | editor.onDidFocusEditorText(() => { 126 | isEditorFocused.set(true); 127 | }); 128 | 129 | const completionProvider = completionItemProvider 130 | 131 | const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = { 132 | ...completionProvider, 133 | provideCompletionItems: (model, position, context, token) => { 134 | if (editor.getModel()?.id !== model.id) { 135 | return { suggestions: [] }; 136 | } 137 | return completionProvider.provideCompletionItems(model, position, context, token); 138 | }, 139 | }; 140 | 141 | const { dispose } = monaco.languages.registerCompletionItemProvider( 142 | SLS_LANG_ID, 143 | filteringCompletionProvider 144 | ); 145 | 146 | autocompleteDisposeFun.current = dispose; 147 | 148 | const updateElementHeight = () => { 149 | const containerDiv = containerRef.current; 150 | if (containerDiv !== null) { 151 | const pixelHeight = editor.getContentHeight(); 152 | containerDiv.style.height = `${pixelHeight + EDITOR_HEIGHT_OFFSET}px`; 153 | containerDiv.style.width = '100%'; 154 | const pixelWidth = containerDiv.clientWidth; 155 | editor.layout({ width: pixelWidth, height: pixelHeight }); 156 | } 157 | }; 158 | 159 | editor.onDidContentSizeChange(updateElementHeight); 160 | updateElementHeight(); 161 | 162 | 163 | const updateCurrentEditorValue = debounce(() => { 164 | const editorValue = editor.getValue(); 165 | onChangeRef.current(editorValue); 166 | }, 300); 167 | 168 | editor.getModel()?.onDidChangeContent(() => { 169 | updateCurrentEditorValue(); 170 | }); 171 | 172 | editor.addCommand( 173 | monaco.KeyMod.Shift | monaco.KeyCode.Enter, 174 | () => { 175 | onRunQueryRef.current(editor.getValue()); 176 | }, 177 | 'isEditorFocused' + id 178 | ); 179 | 180 | if (placeholder) { 181 | const placeholderDecorators = [ 182 | { 183 | range: new monaco.Range(1, 1, 1, 1), 184 | options: { 185 | className: styles.placeholder, 186 | isWholeLine: true, 187 | }, 188 | }, 189 | ]; 190 | 191 | let decorators: string[] = []; 192 | 193 | const checkDecorators: () => void = () => { 194 | const model = editor.getModel(); 195 | 196 | if (!model) { 197 | return; 198 | } 199 | 200 | const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : []; 201 | decorators = model.deltaDecorations(decorators, newDecorators); 202 | }; 203 | 204 | checkDecorators(); 205 | editor.onDidChangeModelContent(checkDecorators); 206 | } 207 | }} 208 | /> 209 |
210 | ); 211 | }; 212 | 213 | 214 | export default MonacoQueryField; 215 | -------------------------------------------------------------------------------- /src/SLS-monaco-editor/MonacoQueryFieldOld.tsx: -------------------------------------------------------------------------------- 1 | import { Button, CodeEditor, Monaco, MonacoEditor } from '@grafana/ui'; 2 | import React, { useCallback, useRef, useState } from 'react'; 3 | import { Props } from './MonacoQueryFieldProps'; 4 | 5 | export class Deferred { 6 | resolve?: (reason?: T | PromiseLike) => void; 7 | reject?: (reason?: any) => void; 8 | promise: Promise; 9 | 10 | constructor() { 11 | this.resolve = undefined; 12 | this.reject = undefined; 13 | 14 | this.promise = new Promise((resolve, reject) => { 15 | this.resolve = resolve as any; 16 | this.reject = reject; 17 | }); 18 | Object.freeze(this); 19 | } 20 | } 21 | 22 | interface MonacoPromise { 23 | editor: MonacoEditor; 24 | monaco: Monaco; 25 | } 26 | 27 | const QueryField: React.FC = ({ 28 | initialValue: query, 29 | onChange, 30 | disableMultiLine, 31 | }) => { 32 | const monacoPromiseRef = useRef>(); 33 | const [queryHeight, setQueryHeight] = useState(30); 34 | const handleEditorMount = useCallback((editor: MonacoEditor, monaco: Monaco) => { 35 | monacoPromiseRef.current?.resolve?.({ editor, monaco }); 36 | }, []); 37 | 38 | return ( 39 |
40 |
41 | 52 |
53 | {!disableMultiLine && ( 54 |
55 |
74 | )} 75 |
76 | ); 77 | }; 78 | 79 | export default QueryField; 80 | -------------------------------------------------------------------------------- /src/SLS-monaco-editor/MonacoQueryFieldProps.ts: -------------------------------------------------------------------------------- 1 | import { HistoryItem } from '@grafana/data'; 2 | 3 | export type Props = { 4 | initialValue: string; 5 | languageProvider: any; 6 | history: Array>; 7 | placeholder: string; 8 | onRunQuery: (value: string) => void; 9 | onBlur: (value: string) => void; 10 | onChange: (value: string) => void; 11 | }; 12 | -------------------------------------------------------------------------------- /src/SLS-monaco-editor/getOverrideServices.ts: -------------------------------------------------------------------------------- 1 | import { monacoTypes } from '@grafana/ui'; 2 | 3 | function makeStorageService() { 4 | const strings = new Map(); 5 | 6 | strings.set('expandSuggestionDocs', true.toString()); 7 | 8 | return { 9 | onDidChangeValue: (data: unknown): void => undefined, 10 | onDidChangeTarget: (data: unknown): void => undefined, 11 | onWillSaveState: (data: unknown): void => undefined, 12 | 13 | get: (key: string, scope: unknown, fallbackValue?: string): string | undefined => { 14 | return strings.get(key) ?? fallbackValue; 15 | }, 16 | 17 | getBoolean: (key: string, scope: unknown, fallbackValue?: boolean): boolean | undefined => { 18 | const val = strings.get(key); 19 | if (val !== undefined) { 20 | return val === 'true'; 21 | } else { 22 | return fallbackValue; 23 | } 24 | }, 25 | 26 | getNumber: (key: string, scope: unknown, fallbackValue?: number): number | undefined => { 27 | const val = strings.get(key); 28 | if (val !== undefined) { 29 | return parseInt(val, 10); 30 | } else { 31 | return fallbackValue; 32 | } 33 | }, 34 | 35 | store: ( 36 | key: string, 37 | value: string | boolean | number | undefined | null, 38 | scope: unknown, 39 | target: unknown 40 | ): void => { 41 | if (value === null || value === undefined) { 42 | strings.delete(key); 43 | } else { 44 | strings.set(key, value.toString()); 45 | } 46 | }, 47 | 48 | remove: (key: string, scope: unknown): void => { 49 | strings.delete(key); 50 | }, 51 | 52 | keys: (scope: unknown, target: unknown): string[] => { 53 | return Array.from(strings.keys()); 54 | }, 55 | 56 | logStorage: (): void => { 57 | console.log('logStorage: not implemented'); 58 | }, 59 | 60 | migrate: (): Promise => { 61 | return Promise.resolve(undefined); 62 | }, 63 | 64 | isNew: (scope: unknown): boolean => { 65 | return true; 66 | }, 67 | 68 | flush: (reason?: unknown): Promise => { 69 | return Promise.resolve(undefined); 70 | }, 71 | }; 72 | } 73 | 74 | let overrideServices: monacoTypes.editor.IEditorOverrideServices | null = null; 75 | 76 | export function getOverrideServices(): monacoTypes.editor.IEditorOverrideServices { 77 | if (overrideServices === null) { 78 | overrideServices = { 79 | storageService: makeStorageService(), 80 | }; 81 | } 82 | 83 | return overrideServices; 84 | } 85 | -------------------------------------------------------------------------------- /src/SLS-monaco-editor/language-definition/definition.ts: -------------------------------------------------------------------------------- 1 | export const slsLanguageDefinition = { 2 | id: 'sls', 3 | extensions: [ '.sls' ], 4 | aliases: [ 'sls', 'SLS', 'SLSQuery' ], 5 | mimetypes: [] 6 | }; 7 | -------------------------------------------------------------------------------- /src/SLS-monaco-editor/language-definition/sls.ts: -------------------------------------------------------------------------------- 1 | import type { monacoTypes } from '@grafana/ui'; 2 | import { functions, keywords, operators } from './slsterms'; 3 | import _ from 'lodash'; 4 | 5 | export const languageConfiguration: monacoTypes.languages.LanguageConfiguration = { 6 | wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+\[{\]}\\|;:'",.<>\/?\s]+)/g, 7 | comments: { 8 | lineComment: '#', 9 | }, 10 | brackets: [ 11 | ['{', '}'], 12 | ['[', ']'], 13 | ['(', ')'], 14 | ], 15 | autoClosingPairs: [ 16 | { open: '{', close: '}' }, 17 | { open: '[', close: ']' }, 18 | { open: '(', close: ')' }, 19 | { open: '"', close: '"' }, 20 | { open: "'", close: "'" }, 21 | ], 22 | surroundingPairs: [ 23 | { open: '{', close: '}' }, 24 | { open: '[', close: ']' }, 25 | { open: '(', close: ')' }, 26 | { open: '"', close: '"' }, 27 | { open: "'", close: "'" }, 28 | { open: '<', close: '>' }, 29 | ], 30 | folding: {}, 31 | }; 32 | 33 | // const sls_functions = functions 34 | 35 | const slsFunctionsList = Object.keys(functions); 36 | const slsFuncitonsSuggest = slsFunctionsList.map((value) => { 37 | return { 38 | label: value, 39 | type: _.get(functions, value).type, 40 | }; 41 | }); 42 | 43 | const slsKeywordsList = keywords.map((value) => { 44 | return value.label; 45 | }); 46 | const slsoperatorList = operators.map((value) => { 47 | return value.label; 48 | }); 49 | const allSuggestions = slsFuncitonsSuggest.concat(keywords).concat(operators); 50 | 51 | export const language = { 52 | ignoreCase: true, 53 | defaultToken: '', 54 | tokenPostfix: '.sls', 55 | vectorMatching: '', 56 | 57 | symbols: /[=>=!%&+\-*/|~^]/, 'operator'], 95 | ], 96 | whitespace: [[/\s+/, 'white']], 97 | comments: [ 98 | [/--+.*/, 'comment'], 99 | [/\/\*/, { token: 'comment.quote', next: '@comment' }], 100 | ], 101 | comment: [ 102 | [/[^*/]+/, 'comment'], 103 | [/\*\//, { token: 'comment.quote', next: '@pop' }], 104 | [/./, 'comment'], 105 | ], 106 | pseudoColumns: [ 107 | [ 108 | /[$][A-Za-z_][\w@#$]*/, 109 | { 110 | cases: { 111 | '@pseudoColumns': 'predefined', 112 | '@default': 'identifier', 113 | }, 114 | }, 115 | ], 116 | ], 117 | numbers: [ 118 | [/0[xX][0-9a-fA-F]*/, 'number'], 119 | [/[$][+-]*\d*(\.\d*)?/, 'number'], 120 | [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, 'number'], 121 | ], 122 | strings: [ 123 | [/N'/, { token: 'string', next: '@string' }], 124 | [/'/, { token: 'string', next: '@string' }], 125 | ], 126 | string: [ 127 | [/[^']+/, 'string'], 128 | [/''/, 'string'], 129 | [/'/, { token: 'string', next: '@pop' }], 130 | ], 131 | complexIdentifiers: [ 132 | [/\[/, { token: 'identifier.quote', next: '@bracketedIdentifier' }], 133 | [/"/, { token: 'identifier.quote', next: '@quotedIdentifier' }], 134 | ], 135 | bracketedIdentifier: [ 136 | [/[^\]]+/, 'identifier'], 137 | [/]]/, 'identifier'], 138 | [/]/, { token: 'identifier.quote', next: '@pop' }], 139 | ], 140 | quotedIdentifier: [ 141 | [/[^"]+/, 'identifier'], 142 | [/""/, 'identifier'], 143 | [/"/, { token: 'identifier.quote', next: '@pop' }], 144 | ], 145 | scopes: [ 146 | [/BEGIN\s+(DISTRIBUTED\s+)?TRAN(SACTION)?\b/i, 'keyword'], 147 | [/BEGIN\s+TRY\b/i, { token: 'keyword.try' }], 148 | [/END\s+TRY\b/i, { token: 'keyword.try' }], 149 | [/BEGIN\s+CATCH\b/i, { token: 'keyword.catch' }], 150 | [/END\s+CATCH\b/i, { token: 'keyword.catch' }], 151 | [/(BEGIN|CASE)\b/i, { token: 'keyword.block' }], 152 | [/END\b/i, { token: 'keyword.block' }], 153 | [/WHEN\b/i, { token: 'keyword.choice' }], 154 | [/THEN\b/i, { token: 'keyword.choice' }], 155 | ], 156 | }, 157 | } as monacoTypes.languages.IMonarchLanguage; 158 | 159 | export const completionItemProvider: monacoTypes.languages.CompletionItemProvider = { 160 | provideCompletionItems: () => { 161 | const suggestions = allSuggestions.map((value) => { 162 | return { 163 | label: value, 164 | kind: value.type, 165 | insertText: value.label, 166 | detail: value.label, 167 | insertTextRules: 4 as monacoTypes.languages.CompletionItemInsertTextRule.InsertAsSnippet, 168 | } as unknown as monacoTypes.languages.CompletionItem; 169 | }); 170 | return { suggestions } as monacoTypes.languages.ProviderResult; 171 | }, 172 | }; 173 | -------------------------------------------------------------------------------- /src/SLS-monaco-editor/language-definition/slsterms.ts: -------------------------------------------------------------------------------- 1 | // type 2 | export enum CompletionItemKind { 3 | Method = 0, 4 | Function = 1, 5 | Constructor = 2, 6 | Field = 3, 7 | Variable = 4, 8 | Class = 5, 9 | Struct = 6, 10 | Interface = 7, 11 | Module = 8, 12 | Property = 9, 13 | Event = 10, 14 | Operator = 11, 15 | Unit = 12, 16 | Value = 13, 17 | Constant = 14, 18 | Enum = 15, 19 | EnumMember = 16, 20 | Keyword = 17, 21 | Text = 18, 22 | Color = 19, 23 | File = 20, 24 | Reference = 21, 25 | Customcolor = 22, 26 | Folder = 23, 27 | TypeParameter = 24, 28 | User = 25, 29 | Issue = 26, 30 | Snippet = 27, 31 | } 32 | 33 | export const keywords = [ 34 | { 35 | label: 'SELECT', 36 | type: CompletionItemKind.Keyword, 37 | }, 38 | { 39 | label: 'FROM', 40 | type: CompletionItemKind.Keyword, 41 | }, 42 | { 43 | label: 'AS', 44 | type: CompletionItemKind.Keyword, 45 | }, 46 | { 47 | label: 'DISTINCT', 48 | type: CompletionItemKind.Keyword, 49 | }, 50 | { 51 | label: 'GROUP', 52 | type: CompletionItemKind.Keyword, 53 | }, 54 | { 55 | label: 'WHERE', 56 | type: CompletionItemKind.Keyword, 57 | }, 58 | { 59 | label: 'BY', 60 | type: CompletionItemKind.Keyword, 61 | }, 62 | { 63 | label: 'HAVING', 64 | type: CompletionItemKind.Keyword, 65 | }, 66 | { 67 | label: 'ORDER', 68 | type: CompletionItemKind.Keyword, 69 | }, 70 | { 71 | label: 'LIMIT', 72 | type: CompletionItemKind.Keyword, 73 | }, 74 | { 75 | label: 'OR', 76 | type: CompletionItemKind.Keyword, 77 | }, 78 | { 79 | label: 'AND', 80 | type: CompletionItemKind.Keyword, 81 | }, 82 | { 83 | label: 'IN', 84 | type: CompletionItemKind.Keyword, 85 | }, 86 | { 87 | label: 'NOT', 88 | type: CompletionItemKind.Keyword, 89 | }, 90 | { 91 | label: 'BETWEEN', 92 | type: CompletionItemKind.Keyword, 93 | }, 94 | { 95 | label: 'LIKE', 96 | type: CompletionItemKind.Keyword, 97 | }, 98 | { 99 | label: 'IS', 100 | type: CompletionItemKind.Keyword, 101 | }, 102 | { 103 | label: 'ASC', 104 | type: CompletionItemKind.Keyword, 105 | }, 106 | { 107 | label: 'DESC', 108 | type: CompletionItemKind.Keyword, 109 | }, 110 | { 111 | label: 'SUBSTRING', 112 | type: CompletionItemKind.Keyword, 113 | }, 114 | { 115 | label: 'CASE', 116 | type: CompletionItemKind.Keyword, 117 | }, 118 | { 119 | label: 'WHEN', 120 | type: CompletionItemKind.Keyword, 121 | }, 122 | { 123 | label: 'THEN', 124 | type: CompletionItemKind.Keyword, 125 | }, 126 | { 127 | label: 'ELSE', 128 | type: CompletionItemKind.Keyword, 129 | }, 130 | { 131 | label: 'END', 132 | type: CompletionItemKind.Keyword, 133 | }, 134 | { 135 | label: 'JOIN', 136 | type: CompletionItemKind.Keyword, 137 | }, 138 | { 139 | label: 'INNER', 140 | type: CompletionItemKind.Keyword, 141 | }, 142 | { 143 | label: 'LEFT', 144 | type: CompletionItemKind.Keyword, 145 | }, 146 | { 147 | label: 'RIGHT', 148 | type: CompletionItemKind.Keyword, 149 | }, 150 | { 151 | label: 'OUTER', 152 | type: CompletionItemKind.Keyword, 153 | }, 154 | { 155 | label: 'ON', 156 | type: CompletionItemKind.Keyword, 157 | }, 158 | { 159 | label: 'UNION', 160 | type: CompletionItemKind.Keyword, 161 | }, 162 | ]; 163 | 164 | export const operators = [ 165 | // Logical 166 | { 167 | label: 'ALL', 168 | type: CompletionItemKind.Operator, 169 | }, 170 | { 171 | label: 'AND', 172 | type: CompletionItemKind.Operator, 173 | }, 174 | { 175 | label: 'ANY', 176 | type: CompletionItemKind.Operator, 177 | }, 178 | { 179 | label: 'BETWEEN', 180 | type: CompletionItemKind.Operator, 181 | }, 182 | { 183 | label: 'IN', 184 | type: CompletionItemKind.Operator, 185 | }, 186 | { 187 | label: 'LIKE', 188 | type: CompletionItemKind.Operator, 189 | }, 190 | { 191 | label: 'NOT', 192 | type: CompletionItemKind.Operator, 193 | }, 194 | { 195 | label: 'OR', 196 | type: CompletionItemKind.Operator, 197 | }, 198 | { 199 | label: 'SOME', 200 | type: CompletionItemKind.Operator, 201 | }, 202 | // Set 203 | { 204 | label: 'EXCEPT', 205 | type: CompletionItemKind.Operator, 206 | }, 207 | { 208 | label: 'INTERSECT', 209 | type: CompletionItemKind.Operator, 210 | }, 211 | { 212 | label: 'UNION', 213 | type: CompletionItemKind.Operator, 214 | }, 215 | // Join 216 | { 217 | label: 'APPLY', 218 | type: CompletionItemKind.Operator, 219 | }, 220 | { 221 | label: 'CROSS', 222 | type: CompletionItemKind.Operator, 223 | }, 224 | { 225 | label: 'FULL', 226 | type: CompletionItemKind.Operator, 227 | }, 228 | { 229 | label: 'INNER', 230 | type: CompletionItemKind.Operator, 231 | }, 232 | { 233 | label: 'JOIN', 234 | type: CompletionItemKind.Operator, 235 | }, 236 | { 237 | label: 'LEFT', 238 | type: CompletionItemKind.Operator, 239 | }, 240 | { 241 | label: 'OUTER', 242 | type: CompletionItemKind.Operator, 243 | }, 244 | { 245 | label: 'RIGHT', 246 | type: CompletionItemKind.Operator, 247 | }, 248 | // Predicates 249 | { 250 | label: 'CONTAINS', 251 | type: CompletionItemKind.Operator, 252 | }, 253 | { 254 | label: 'FREETEXT', 255 | type: CompletionItemKind.Operator, 256 | }, 257 | { 258 | label: 'IS', 259 | type: CompletionItemKind.Operator, 260 | }, 261 | { 262 | label: 'NULL', 263 | type: CompletionItemKind.Operator, 264 | }, 265 | // Pivoting 266 | { 267 | label: 'PIVOT', 268 | type: CompletionItemKind.Operator, 269 | }, 270 | { 271 | label: 'UNPIVOT', 272 | type: CompletionItemKind.Operator, 273 | }, 274 | // Merging 275 | { 276 | label: 'MATCHED', 277 | type: CompletionItemKind.Operator, 278 | }, 279 | ]; 280 | 281 | export const functions = { 282 | // 通用聚合函数 283 | arbitrary: { 284 | type: CompletionItemKind.Field, 285 | }, 286 | avg: { 287 | type: CompletionItemKind.Field, 288 | }, 289 | checksum: { 290 | type: CompletionItemKind.Field, 291 | }, 292 | COUNT: { 293 | type: CompletionItemKind.Field, 294 | }, 295 | count_if: { 296 | type: CompletionItemKind.Field, 297 | }, 298 | geometric_mean: { 299 | type: CompletionItemKind.Field, 300 | }, 301 | max_by: { 302 | type: CompletionItemKind.Field, 303 | }, 304 | min_by: { 305 | type: CompletionItemKind.Field, 306 | }, 307 | max: { 308 | type: CompletionItemKind.Field, 309 | }, 310 | min: { 311 | type: CompletionItemKind.Field, 312 | }, 313 | sum: { 314 | type: CompletionItemKind.Field, 315 | }, 316 | bitwise_and_agg: { 317 | type: CompletionItemKind.Field, 318 | }, 319 | bitwise_or_agg: { 320 | type: CompletionItemKind.Field, 321 | }, 322 | // 映射函数 323 | histogram: { 324 | type: CompletionItemKind.Method, 325 | }, 326 | map_agg: { 327 | type: CompletionItemKind.Method, 328 | }, 329 | multimap_agg: { 330 | type: CompletionItemKind.Method, 331 | }, 332 | cardinality: { 333 | type: CompletionItemKind.Method, 334 | }, 335 | element_at: { 336 | type: CompletionItemKind.Method, 337 | }, 338 | map: { 339 | type: CompletionItemKind.Method, 340 | }, 341 | map_from_entries: { 342 | type: CompletionItemKind.Method, 343 | }, 344 | map_entries: { 345 | type: CompletionItemKind.Method, 346 | }, 347 | map_concat: { 348 | type: CompletionItemKind.Method, 349 | }, 350 | map_filter: { 351 | type: CompletionItemKind.Method, 352 | }, 353 | transform_keys: { 354 | type: CompletionItemKind.Method, 355 | }, 356 | transform_values: { 357 | type: CompletionItemKind.Method, 358 | }, 359 | map_keys: { 360 | type: CompletionItemKind.Method, 361 | }, 362 | map_values: { 363 | type: CompletionItemKind.Method, 364 | }, 365 | map_zip_with: { 366 | type: CompletionItemKind.Method, 367 | }, 368 | // 估算函数 369 | approx_distinct: { 370 | type: CompletionItemKind.Method, 371 | }, 372 | approx_percentile: { 373 | type: CompletionItemKind.Method, 374 | }, 375 | numeric_histogram: { 376 | type: CompletionItemKind.Method, 377 | }, 378 | // 数学统计函数 379 | corr: { 380 | type: CompletionItemKind.Function, 381 | }, 382 | covar_pop: { 383 | type: CompletionItemKind.Function, 384 | }, 385 | covar_samp: { 386 | type: CompletionItemKind.Function, 387 | }, 388 | regr_intercept: { 389 | type: CompletionItemKind.Function, 390 | }, 391 | regr_slope: { 392 | type: CompletionItemKind.Function, 393 | }, 394 | stddev: { 395 | type: CompletionItemKind.Function, 396 | }, 397 | stddev_samp: { 398 | type: CompletionItemKind.Function, 399 | }, 400 | stddev_pop: { 401 | type: CompletionItemKind.Function, 402 | }, 403 | variance: { 404 | type: CompletionItemKind.Function, 405 | }, 406 | var_samp: { 407 | type: CompletionItemKind.Function, 408 | }, 409 | var_pop: { 410 | type: CompletionItemKind.Function, 411 | }, 412 | // 数学计算函数 413 | abs: { 414 | type: CompletionItemKind.Function, 415 | }, 416 | cbrt: { 417 | type: CompletionItemKind.Function, 418 | }, 419 | ceiling: { 420 | type: CompletionItemKind.Function, 421 | }, 422 | cosine_similarity: { 423 | type: CompletionItemKind.Function, 424 | }, 425 | degrees: { 426 | type: CompletionItemKind.Function, 427 | }, 428 | e: { 429 | type: CompletionItemKind.Function, 430 | }, 431 | exp: { 432 | type: CompletionItemKind.Function, 433 | }, 434 | floor: { 435 | type: CompletionItemKind.Function, 436 | }, 437 | from_base: { 438 | type: CompletionItemKind.Function, 439 | }, 440 | ln: { 441 | type: CompletionItemKind.Function, 442 | }, 443 | log2: { 444 | type: CompletionItemKind.Function, 445 | }, 446 | log10: { 447 | type: CompletionItemKind.Function, 448 | }, 449 | log: { 450 | type: CompletionItemKind.Function, 451 | }, 452 | pi: { 453 | type: CompletionItemKind.Function, 454 | }, 455 | pow: { 456 | type: CompletionItemKind.Function, 457 | }, 458 | radians: { 459 | type: CompletionItemKind.Function, 460 | }, 461 | rand: { 462 | type: CompletionItemKind.Function, 463 | }, 464 | random: { 465 | type: CompletionItemKind.Function, 466 | }, 467 | round: { 468 | type: CompletionItemKind.Function, 469 | }, 470 | sqrt: { 471 | type: CompletionItemKind.Function, 472 | }, 473 | to_base: { 474 | type: CompletionItemKind.Function, 475 | }, 476 | truncate: { 477 | type: CompletionItemKind.Function, 478 | }, 479 | acos: { 480 | type: CompletionItemKind.Function, 481 | }, 482 | asin: { 483 | type: CompletionItemKind.Function, 484 | }, 485 | atan: { 486 | type: CompletionItemKind.Function, 487 | }, 488 | atan2: { 489 | type: CompletionItemKind.Function, 490 | }, 491 | cos: { 492 | type: CompletionItemKind.Function, 493 | }, 494 | sin: { 495 | type: CompletionItemKind.Function, 496 | }, 497 | cosh: { 498 | type: CompletionItemKind.Function, 499 | }, 500 | tan: { 501 | type: CompletionItemKind.Function, 502 | }, 503 | tanh: { 504 | type: CompletionItemKind.Function, 505 | }, 506 | infinity: { 507 | type: CompletionItemKind.Function, 508 | }, 509 | is_infinity: { 510 | type: CompletionItemKind.Function, 511 | }, 512 | is_finity: { 513 | type: CompletionItemKind.Function, 514 | }, 515 | is_nan: { 516 | type: CompletionItemKind.Function, 517 | }, 518 | // 字符串函数 519 | chr: { 520 | type: CompletionItemKind.Function, 521 | }, 522 | length: { 523 | type: CompletionItemKind.Function, 524 | }, 525 | levenshtein_distance: { 526 | type: CompletionItemKind.Function, 527 | }, 528 | lpad: { 529 | type: CompletionItemKind.Function, 530 | }, 531 | rpad: { 532 | type: CompletionItemKind.Function, 533 | }, 534 | ltrim: { 535 | type: CompletionItemKind.Function, 536 | }, 537 | replace: { 538 | type: CompletionItemKind.Function, 539 | }, 540 | reverse: { 541 | type: CompletionItemKind.Function, 542 | }, 543 | rtrim: { 544 | type: CompletionItemKind.Function, 545 | }, 546 | split: { 547 | type: CompletionItemKind.Function, 548 | }, 549 | split_part: { 550 | type: CompletionItemKind.Function, 551 | }, 552 | split_to_map: { 553 | type: CompletionItemKind.Function, 554 | }, 555 | position: { 556 | type: CompletionItemKind.Function, 557 | }, 558 | strpos: { 559 | type: CompletionItemKind.Function, 560 | }, 561 | substr: { 562 | type: CompletionItemKind.Function, 563 | }, 564 | trim: { 565 | type: CompletionItemKind.Function, 566 | }, 567 | upper: { 568 | type: CompletionItemKind.Function, 569 | }, 570 | concat: { 571 | type: CompletionItemKind.Function, 572 | }, 573 | hamming_distance: { 574 | type: CompletionItemKind.Function, 575 | }, 576 | // 日期函数 577 | current_date: { 578 | type: CompletionItemKind.Function, 579 | }, 580 | current_time: { 581 | type: CompletionItemKind.Function, 582 | }, 583 | current_timestamp: { 584 | type: CompletionItemKind.Function, 585 | }, 586 | current_timezone: { 587 | type: CompletionItemKind.Function, 588 | }, 589 | from_iso8601_timestamp: { 590 | type: CompletionItemKind.Function, 591 | }, 592 | from_iso8601_date: { 593 | type: CompletionItemKind.Function, 594 | }, 595 | from_unixtime: { 596 | type: CompletionItemKind.Function, 597 | }, 598 | localtime: { 599 | type: CompletionItemKind.Function, 600 | }, 601 | localtimestamp: { 602 | type: CompletionItemKind.Function, 603 | }, 604 | now: { 605 | type: CompletionItemKind.Function, 606 | }, 607 | to_unixtime: { 608 | type: CompletionItemKind.Function, 609 | }, 610 | date_trunc: { 611 | type: CompletionItemKind.Function, 612 | }, 613 | date_format: { 614 | type: CompletionItemKind.Function, 615 | }, 616 | date_parse: { 617 | type: CompletionItemKind.Function, 618 | }, 619 | date_add: { 620 | type: CompletionItemKind.Function, 621 | }, 622 | date_diff: { 623 | type: CompletionItemKind.Function, 624 | }, 625 | time_series: { 626 | type: CompletionItemKind.Function, 627 | }, 628 | // URL函数 629 | url_extract_fragment: { 630 | type: CompletionItemKind.Function, 631 | }, 632 | url_extract_host: { 633 | type: CompletionItemKind.Function, 634 | }, 635 | url_extract_parameter: { 636 | type: CompletionItemKind.Function, 637 | }, 638 | url_extract_path: { 639 | type: CompletionItemKind.Function, 640 | }, 641 | url_extract_port: { 642 | type: CompletionItemKind.Function, 643 | }, 644 | url_extract_protocol: { 645 | type: CompletionItemKind.Function, 646 | }, 647 | url_extract_query: { 648 | type: CompletionItemKind.Function, 649 | }, 650 | url_encode: { 651 | type: CompletionItemKind.Function, 652 | }, 653 | url_decode: { 654 | type: CompletionItemKind.Function, 655 | }, 656 | // 正则式函数 657 | regexp_extract_all: { 658 | type: CompletionItemKind.Function, 659 | }, 660 | regexp_extract: { 661 | type: CompletionItemKind.Function, 662 | }, 663 | regexp_like: { 664 | type: CompletionItemKind.Function, 665 | }, 666 | regexp_replace: { 667 | type: CompletionItemKind.Function, 668 | }, 669 | regexp_split: { 670 | type: CompletionItemKind.Function, 671 | }, 672 | // JSON函数 673 | json_parse: { 674 | type: CompletionItemKind.Function, 675 | }, 676 | json_format: { 677 | type: CompletionItemKind.Function, 678 | }, 679 | json_array_contains: { 680 | type: CompletionItemKind.Function, 681 | }, 682 | json_array_get: { 683 | type: CompletionItemKind.Function, 684 | }, 685 | json_array_length: { 686 | type: CompletionItemKind.Function, 687 | }, 688 | json_extract: { 689 | type: CompletionItemKind.Function, 690 | }, 691 | json_extract_scalar: { 692 | type: CompletionItemKind.Function, 693 | }, 694 | json_size: { 695 | type: CompletionItemKind.Function, 696 | }, 697 | // 类型转换函数 698 | cast: { 699 | type: CompletionItemKind.Method, 700 | }, 701 | try_cast: { 702 | type: CompletionItemKind.Method, 703 | }, 704 | // IP地理函数 705 | ip_to_domain: { 706 | type: CompletionItemKind.Function, 707 | }, 708 | ip_to_country: { 709 | type: CompletionItemKind.Function, 710 | }, 711 | ip_to_province: { 712 | type: CompletionItemKind.Function, 713 | }, 714 | ip_to_city: { 715 | type: CompletionItemKind.Function, 716 | }, 717 | ip_to_geo: { 718 | type: CompletionItemKind.Function, 719 | }, 720 | ip_to_city_geo: { 721 | type: CompletionItemKind.Function, 722 | }, 723 | ip_to_provider: { 724 | type: CompletionItemKind.Function, 725 | }, 726 | ip_to_country_code: { 727 | type: CompletionItemKind.Function, 728 | }, 729 | // 同比和环比函数 730 | compare: { 731 | type: CompletionItemKind.Function, 732 | }, 733 | // 机器学习函数 734 | ts_smooth_simple: { 735 | type: CompletionItemKind.Interface, 736 | }, 737 | ts_smooth_fir: { 738 | type: CompletionItemKind.Interface, 739 | }, 740 | ts_smooth_iir: { 741 | type: CompletionItemKind.Interface, 742 | }, 743 | ts_period_detect: { 744 | type: CompletionItemKind.Interface, 745 | }, 746 | ts_cp_detect: { 747 | type: CompletionItemKind.Interface, 748 | }, 749 | ts_breakout_detect: { 750 | type: CompletionItemKind.Interface, 751 | }, 752 | ts_predicate_simple: { 753 | type: CompletionItemKind.Interface, 754 | }, 755 | ts_predicate_ar: { 756 | type: CompletionItemKind.Interface, 757 | }, 758 | ts_predicate_arma: { 759 | type: CompletionItemKind.Interface, 760 | }, 761 | ts_predicate_arima: { 762 | type: CompletionItemKind.Interface, 763 | }, 764 | ts_regression_predict: { 765 | type: CompletionItemKind.Interface, 766 | }, 767 | ts_decompose: { 768 | type: CompletionItemKind.Interface, 769 | }, 770 | ts_density_cluster: { 771 | type: CompletionItemKind.Interface, 772 | }, 773 | ts_hierarchical_cluster: { 774 | type: CompletionItemKind.Interface, 775 | }, 776 | ts_similar_instance: { 777 | type: CompletionItemKind.Interface, 778 | }, 779 | pattern_stat: { 780 | type: CompletionItemKind.Interface, 781 | }, 782 | pattern_diff: { 783 | type: CompletionItemKind.Interface, 784 | }, 785 | ts_find_peaks: { 786 | type: CompletionItemKind.Interface, 787 | }, 788 | // PromQL函数 789 | promql_query: { 790 | type: CompletionItemKind.Struct, 791 | }, 792 | promql_query_range: { 793 | type: CompletionItemKind.Struct, 794 | }, 795 | promql_labels: { 796 | type: CompletionItemKind.Struct, 797 | }, 798 | promql_label_values: { 799 | type: CompletionItemKind.Struct, 800 | }, 801 | promql_series: { 802 | type: CompletionItemKind.Struct, 803 | }, 804 | }; 805 | -------------------------------------------------------------------------------- /src/SelectTips.tsx: -------------------------------------------------------------------------------- 1 | import { Card, SeriesTable, SeriesTableRowProps } from '@grafana/ui'; 2 | import { version, xColInfoSeries, yColInfoSeries, totalLogsInfoSeries } from 'const'; 3 | import React from 'react'; 4 | 5 | export function SelectTips(props: { type: string }) { 6 | const isOld = 7 | version === '' || 8 | version.startsWith('8.0') || 9 | version.startsWith('8.1') || 10 | version.startsWith('8.2') || 11 | version.startsWith('8.3') || 12 | version.startsWith('7'); 13 | let series = [] as SeriesTableRowProps[] 14 | switch(props.type) { 15 | case 'ycol': 16 | series = yColInfoSeries 17 | break 18 | case 'xcol': 19 | series = xColInfoSeries 20 | break 21 | case 'totalLogs': 22 | series = totalLogsInfoSeries 23 | break 24 | default: 25 | break 26 | } 27 | return isOld ? ( 28 | 29 | {series.map((v, i) => { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | })} 38 |
{`${i + 1}.`}{v.label}{v.value}
39 | ) : ( 40 | 41 | {`${props.type} 简介 Introduction`} 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/VariableQueryEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, PureComponent } from 'react'; 2 | import { SLSDataSourceOptions, SLSQuery } from './types'; 3 | 4 | import { InlineFormLabel } from '@grafana/ui'; 5 | import { QueryEditorProps } from '@grafana/data'; 6 | import { SLSDataSource } from './datasource'; 7 | import MonacoQueryField from 'SLS-monaco-editor/MonacoQueryField'; 8 | import MonacoQueryFieldOld from 'SLS-monaco-editor/MonacoQueryFieldOld'; 9 | import { version } from 'const'; 10 | import { SLSQueryEditor } from 'QueryEditor'; 11 | // const { FormField } = LegacyForms; 12 | 13 | type Props = QueryEditorProps; 14 | 15 | export class SLSVariableQueryEditor extends PureComponent { 16 | onQueryTextChange = (event: ChangeEvent) => { 17 | const { onChange, query } = this.props; 18 | onChange({ ...query, query: event.target.value }); 19 | }; 20 | 21 | onQueryTextChangeString = (value: string) => { 22 | const { onChange, query } = this.props; 23 | onChange({ ...query, query: value }); 24 | }; 25 | 26 | onQueryTextChangeWithRunQuery = (value: string) => { 27 | const { onChange, query, onRunQuery } = this.props; 28 | onChange({ ...query, query: value }); 29 | // executes the query 30 | onRunQuery(); 31 | }; 32 | 33 | render() { 34 | const { query } = this.props.query; 35 | 36 | return ( 37 | <> 38 |
39 | {/* */} 47 | 48 | Query 49 | 50 | {version === '' || 51 | version.startsWith('8.0') || 52 | version.startsWith('8.1') || 53 | version.startsWith('8.2') || 54 | version.startsWith('7') ? ( 55 | // 62 |
63 | 73 |
74 | ) : ( 75 | 84 | )} 85 |
86 | 87 | ); 88 | } 89 | } 90 | 91 | 92 | 93 | 94 | export function SLSVariableQueryEditorWapper (props: Props) { 95 | return 96 | } 97 | -------------------------------------------------------------------------------- /src/components/Collapse.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import React, { Component } from 'react'; 3 | 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | 6 | import { Icon, withTheme2 } from '@grafana/ui'; 7 | 8 | const getStyles = (theme: GrafanaTheme2) => ({ 9 | collapse: css` 10 | label: collapse; 11 | margin-bottom: ${theme.spacing(1)}; 12 | `, 13 | collapseBody: css` 14 | label: collapse__body; 15 | padding: ${theme.spacing(theme.components.panel.padding)}; 16 | padding-top: 0; 17 | flex: 1; 18 | overflow: hidden; 19 | display: flex; 20 | flex-direction: column; 21 | `, 22 | bodyContentWrapper: css` 23 | label: bodyContentWrapper; 24 | flex: 1; 25 | overflow: hidden; 26 | `, 27 | loader: css` 28 | label: collapse__loader; 29 | height: 2px; 30 | position: relative; 31 | overflow: hidden; 32 | background: none; 33 | margin: ${theme.spacing(0.5)}; 34 | `, 35 | loaderActive: css` 36 | label: collapse__loader_active; 37 | &:after { 38 | content: ' '; 39 | display: block; 40 | width: 25%; 41 | top: 0; 42 | top: -50%; 43 | height: 250%; 44 | position: absolute; 45 | animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67) 500ms; 46 | animation-iteration-count: 100; 47 | left: -25%; 48 | background: ${theme.colors.primary.main}; 49 | } 50 | @keyframes loader { 51 | from { 52 | left: -25%; 53 | opacity: 0.1; 54 | } 55 | to { 56 | left: 100%; 57 | opacity: 1; 58 | } 59 | } 60 | `, 61 | header: css` 62 | label: collapse__header; 63 | padding: ${theme.spacing(1, 2, 1, 2)}; 64 | display: flex; 65 | cursor: inherit; 66 | transition: all 0.1s linear; 67 | cursor: pointer; 68 | `, 69 | headerCollapsed: css` 70 | label: collapse__header--collapsed; 71 | padding: ${theme.spacing(1, 2, 1, 2)}; 72 | `, 73 | headerLabel: css` 74 | label: collapse__header-label; 75 | font-weight: ${theme.typography.fontWeightMedium}; 76 | margin-right: ${theme.spacing(1)}; 77 | font-size: ${theme.typography.size.md}; 78 | `, 79 | icon: css` 80 | label: collapse__icon; 81 | margin: ${theme.spacing(0, 1, 0, -1)}; 82 | `, 83 | }); 84 | 85 | export interface Props { 86 | /** Expand or collapse the content */ 87 | isOpen?: boolean; 88 | /** Element or text for the Collapse header */ 89 | label: React.ReactNode; 90 | /** Indicates loading state of the content */ 91 | loading?: boolean; 92 | /** Toggle collapsed header icon */ 93 | collapsible?: boolean; 94 | /** Callback for the toggle functionality */ 95 | onToggle?: (isOpen: boolean) => void; 96 | /** Additional class name for the root element */ 97 | className?: string; 98 | 99 | theme: GrafanaTheme2; 100 | } 101 | 102 | class Collapse extends Component { 103 | static defaultProps = { 104 | isOpen: false, 105 | collapsible: true, 106 | loading: false, 107 | }; 108 | 109 | onClickToggle = () => { 110 | const { onToggle, isOpen } = this.props; 111 | if (onToggle) { 112 | onToggle(!isOpen); 113 | } 114 | }; 115 | 116 | render() { 117 | const { isOpen, label, loading, collapsible, className, children, theme } = this.props; 118 | const styles = getStyles(theme); 119 | 120 | const panelClass = cx([styles.collapse, 'panel-container', className]); 121 | const loaderClass = loading ? cx([styles.loader, styles.loaderActive]) : cx([styles.loader]); 122 | const headerClass = collapsible ? cx([styles.header]) : cx([styles.headerCollapsed]); 123 | 124 | return ( 125 |
126 |
127 | {collapsible && } 128 |
{label}
129 |
130 | {isOpen && ( 131 |
132 |
133 |
{children}
134 |
135 | )} 136 |
137 | ); 138 | } 139 | } 140 | 141 | const CollapseWithTheme = withTheme2(Collapse); 142 | 143 | 144 | export { CollapseWithTheme as Collapse }; 145 | 146 | -------------------------------------------------------------------------------- /src/components/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import React, { Component } from 'react'; 3 | 4 | import { GrafanaTheme2, colorManipulator } from '@grafana/data'; 5 | import { getFocusStyles, getMouseFocusStyles } from './RadioButtonGroup/RadioButton'; 6 | import { Icon, IconName, IconSize, IconType, PopoverContent, Tooltip, withTheme2 } from '@grafana/ui'; 7 | 8 | export type IconButtonVariant = 'primary' | 'secondary' | 'destructive'; 9 | 10 | export interface Props extends React.ButtonHTMLAttributes { 11 | /** Name of the icon **/ 12 | name: IconName; 13 | /** Icon size */ 14 | size?: IconSize; 15 | /** Type of the icon - mono or default */ 16 | iconType?: IconType; 17 | /** Tooltip content to display on hover */ 18 | tooltip?: PopoverContent; 19 | /** Position of the tooltip */ 20 | tooltipPlacement?: TooltipPlacement; 21 | /** Variant to change the color of the Icon */ 22 | variant?: IconButtonVariant; 23 | /** Text available only for screen readers. Will use tooltip text as fallback. */ 24 | ariaLabel?: string; 25 | /** Theme object injected by withTheme2 HOC */ 26 | theme: GrafanaTheme2; 27 | innerRef?: React.Ref; 28 | } 29 | 30 | class IconButton_ extends Component { 31 | static defaultProps = { 32 | size: 'md' as any, 33 | variant: 'secondary' as any, 34 | }; 35 | 36 | getStyles = () => { 37 | const { theme, size, variant } = this.props; 38 | const pixelSize = getSvgSize(size || 'md'); 39 | const hoverSize = Math.max(pixelSize / 3, 8); 40 | let iconColor = theme.colors.text.primary; 41 | 42 | if (variant === 'primary') { 43 | iconColor = theme.colors.primary.text; 44 | } else if (variant === 'destructive') { 45 | iconColor = theme.colors.error.text; 46 | } 47 | 48 | return { 49 | button: css` 50 | width: ${pixelSize}px; 51 | height: ${pixelSize}px; 52 | background: transparent; 53 | border: none; 54 | color: ${iconColor}; 55 | padding: 0; 56 | margin: 0; 57 | outline: none; 58 | box-shadow: none; 59 | display: inline-flex; 60 | align-items: center; 61 | justify-content: center; 62 | position: relative; 63 | border-radius: ${theme.shape.borderRadius()}; 64 | z-index: 0; 65 | margin-right: ${theme.spacing(0.5)}; 66 | 67 | &[disabled], 68 | &:disabled { 69 | cursor: not-allowed; 70 | color: ${theme.colors.action.disabledText}; 71 | opacity: 0.65; 72 | box-shadow: none; 73 | } 74 | 75 | &:before { 76 | content: ''; 77 | display: block; 78 | opacity: 1; 79 | position: absolute; 80 | transition-duration: 0.2s; 81 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 82 | z-index: -1; 83 | bottom: -${hoverSize}px; 84 | left: -${hoverSize}px; 85 | right: -${hoverSize}px; 86 | top: -${hoverSize}px; 87 | background: none; 88 | border-radius: 50%; 89 | box-sizing: border-box; 90 | transform: scale(0); 91 | transition-property: transform, opacity; 92 | } 93 | 94 | &:focus, 95 | &:focus-visible { 96 | ${getFocusStyles(theme)} 97 | } 98 | 99 | &:focus:not(:focus-visible) { 100 | ${getMouseFocusStyles(theme)} 101 | } 102 | 103 | &:hover { 104 | color: ${iconColor}; 105 | 106 | &:before { 107 | background-color: ${variant === 'secondary' 108 | ? theme.colors.action.hover 109 | : colorManipulator.alpha(iconColor, 0.12)}; 110 | border: none; 111 | box-shadow: none; 112 | opacity: 1; 113 | transform: scale(0.8); 114 | } 115 | } 116 | `, 117 | icon: css` 118 | vertical-align: baseline; 119 | display: flex; 120 | `, 121 | }; 122 | }; 123 | 124 | render() { 125 | const { 126 | name, 127 | size = 'md', 128 | iconType, 129 | tooltip, 130 | tooltipPlacement, 131 | ariaLabel, 132 | className, 133 | variant = 'secondary', 134 | theme, 135 | ...restProps 136 | } = this.props; 137 | 138 | const styles = this.getStyles(); 139 | const tooltipString = typeof tooltip === 'string' ? tooltip : ''; 140 | 141 | const button = ( 142 | 145 | ); 146 | 147 | if (tooltip) { 148 | return ( 149 | 150 | {button} 151 | 152 | ); 153 | } 154 | 155 | return button; 156 | } 157 | } 158 | 159 | 160 | 161 | export const IconButton = withTheme2(IconButton_); 162 | 163 | /* Transform string with px to number and add 2 pxs as path in svg is 2px smaller */ 164 | export function getSvgSize(size: IconSize) { 165 | switch (size) { 166 | case 'xs': 167 | return 12; 168 | case 'sm': 169 | return 14; 170 | case 'md': 171 | return 16; 172 | case 'lg': 173 | return 18; 174 | case 'xl': 175 | return 24; 176 | case 'xxl': 177 | return 36; 178 | case 'xxxl': 179 | return 48; 180 | } 181 | } 182 | 183 | export type TooltipPlacement = 184 | | 'auto-start' 185 | | 'auto' 186 | | 'auto-end' 187 | | 'top-start' 188 | | 'top' 189 | | 'top-end' 190 | | 'right-start' 191 | | 'right' 192 | | 'right-end' 193 | | 'bottom-end' 194 | | 'bottom' 195 | | 'bottom-start' 196 | | 'left-end' 197 | | 'left' 198 | | 'left-start'; 199 | -------------------------------------------------------------------------------- /src/components/QueryEditor/EditorField.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import React, { Component, ComponentProps } from 'react'; 3 | import { GrafanaTheme2 } from '@grafana/data'; 4 | import Space from './Space'; 5 | import { Icon, PopoverContent, ReactUtils, Tooltip, withTheme2 } from '@grafana/ui'; 6 | import Field from './Field' 7 | 8 | interface EditorFieldProps extends ComponentProps { 9 | label: string; 10 | children: React.ReactElement; 11 | width?: number | string; 12 | optional?: boolean; 13 | tooltip?: PopoverContent; 14 | tooltipInteractive?: boolean; 15 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 16 | } 17 | 18 | class EditorField extends Component { 19 | getStyles = () => { 20 | const { theme, width } = this.props; 21 | return { 22 | root: css({ 23 | minWidth: theme.spacing(width ?? 0), 24 | }), 25 | label: css({ 26 | fontSize: 12, 27 | fontWeight: theme.typography.fontWeightMedium, 28 | }), 29 | optional: css({ 30 | fontStyle: 'italic', 31 | color: theme.colors.text.secondary, 32 | }), 33 | field: css({ 34 | marginBottom: 0, // GrafanaUI/Field has a bottom margin which we must remove 35 | }), 36 | icon: css({ 37 | color: theme.colors.text.secondary, 38 | marginLeft: theme.spacing(1), 39 | ':hover': { 40 | color: theme.colors.text.primary, 41 | }, 42 | }), 43 | }; 44 | }; 45 | 46 | render() { 47 | const { label, optional, tooltip, tooltipInteractive, children, width, theme, ...fieldProps } = this.props; 48 | 49 | const styles = this.getStyles(); 50 | 51 | // Null check for backward compatibility 52 | const childInputId = fieldProps?.htmlFor || ReactUtils?.getChildId?.(children); 53 | 54 | const labelEl = ( 55 | <> 56 | 65 | 66 | 67 | ); 68 | 69 | return ( 70 |
71 | 72 | {children} 73 | 74 |
75 | ); 76 | } 77 | } 78 | 79 | export default withTheme2(EditorField); 80 | -------------------------------------------------------------------------------- /src/components/QueryEditor/EditorRow.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import React, { Component } from 'react'; 3 | import { withTheme2 } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | 6 | import Stack from './Stack'; 7 | 8 | interface EditorRowProps { 9 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 10 | } 11 | 12 | interface EditorRowState {} 13 | 14 | class EditorRow extends Component, EditorRowState> { 15 | getStyles = () => { 16 | const { theme } = this.props; 17 | 18 | return { 19 | root: css({ 20 | backgroundColor: theme.colors.background.secondary, 21 | borderRadius: theme.shape.borderRadius(1), 22 | }), 23 | }; 24 | }; 25 | 26 | render() { 27 | const { children } = this.props; 28 | const styles = this.getStyles(); 29 | 30 | return ( 31 |
32 | {children} 33 |
34 | ); 35 | } 36 | } 37 | 38 | export default withTheme2(EditorRow); 39 | -------------------------------------------------------------------------------- /src/components/QueryEditor/Field.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import React, { Component, HTMLAttributes } from 'react'; 3 | import { withTheme2, ReactUtils } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | 6 | import FieldValidationMessage from './FieldValidationMessage'; 7 | import Label from './Label'; 8 | 9 | export interface FieldProps extends HTMLAttributes { 10 | /** Form input element, i.e Input or Switch */ 11 | children: React.ReactElement; 12 | /** Label for the field */ 13 | label?: React.ReactNode; 14 | /** Description of the field */ 15 | description?: React.ReactNode; 16 | /** Indicates if field is in invalid state */ 17 | invalid?: boolean; 18 | /** Indicates if field is in loading state */ 19 | loading?: boolean; 20 | /** Indicates if field is disabled */ 21 | disabled?: boolean; 22 | /** Indicates if field is required */ 23 | required?: boolean; 24 | /** Error message to display */ 25 | error?: React.ReactNode; 26 | /** Indicates horizontal layout of the field */ 27 | horizontal?: boolean; 28 | /** make validation message overflow horizontally. Prevents pushing out adjacent inline components */ 29 | validationMessageHorizontalOverflow?: boolean; 30 | 31 | className?: string; 32 | /** 33 | * A unique id that associates the label of the Field component with the control with the unique id. 34 | * If the `htmlFor` property is missing the `htmlFor` will be inferred from the `id` or `inputId` property of the first child. 35 | * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label#attr-for 36 | */ 37 | htmlFor?: string; 38 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 39 | } 40 | 41 | class Field extends Component { 42 | static defaultProps = { 43 | validationMessageHorizontalOverflow: false, 44 | }; 45 | 46 | getStyles = () => { 47 | const { theme } = this.props; 48 | return { 49 | field: css` 50 | display: flex; 51 | flex-direction: column; 52 | margin-bottom: ${theme.spacing(2)}; 53 | `, 54 | fieldHorizontal: css` 55 | flex-direction: row; 56 | justify-content: space-between; 57 | flex-wrap: wrap; 58 | `, 59 | fieldValidationWrapper: css` 60 | margin-top: ${theme.spacing(0.5)}; 61 | `, 62 | fieldValidationWrapperHorizontal: css` 63 | flex: 1 1 100%; 64 | `, 65 | validationMessageHorizontalOverflow: css` 66 | width: 0; 67 | overflow-x: visible; 68 | 69 | & > * { 70 | white-space: nowrap; 71 | } 72 | `, 73 | }; 74 | }; 75 | 76 | deleteUndefinedProps = (obj: T): Partial => { 77 | for (const key in obj) { 78 | if (obj[key] === undefined) { 79 | delete obj[key]; 80 | } 81 | } 82 | return obj; 83 | }; 84 | 85 | render() { 86 | const { 87 | label, 88 | description, 89 | horizontal, 90 | invalid, 91 | loading, 92 | disabled, 93 | required, 94 | error, 95 | children, 96 | className, 97 | validationMessageHorizontalOverflow, 98 | htmlFor, 99 | theme, 100 | ...otherProps 101 | } = this.props; 102 | 103 | const styles = this.getStyles(); 104 | const inputId = htmlFor ?? ReactUtils?.getChildId?.(children); 105 | 106 | const labelElement = 107 | typeof label === 'string' ? ( 108 | 111 | ) : ( 112 | label 113 | ); 114 | 115 | const childProps = this.deleteUndefinedProps({ invalid, disabled, loading }); 116 | 117 | return ( 118 |
119 | {labelElement} 120 |
121 | {React.cloneElement(children, childProps)} 122 | {invalid && error && !horizontal && ( 123 |
128 | {error} 129 |
130 | )} 131 |
132 | 133 | {invalid && error && horizontal && ( 134 |
139 | {error} 140 |
141 | )} 142 |
143 | ); 144 | } 145 | } 146 | 147 | export default withTheme2(Field); 148 | -------------------------------------------------------------------------------- /src/components/QueryEditor/FieldValidationMessage.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import React, { Component } from 'react'; 3 | import { GrafanaTheme2 } from '@grafana/data'; 4 | 5 | import { stylesFactory, Icon, withTheme2 } from '@grafana/ui'; 6 | 7 | export interface FieldValidationMessageProps { 8 | /** Override component style */ 9 | className?: string; 10 | horizontal?: boolean; 11 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 12 | } 13 | 14 | const getFieldValidationMessageStyles = stylesFactory((theme: GrafanaTheme2) => { 15 | const baseStyle = ` 16 | font-size: ${theme.typography.size.sm}; 17 | font-weight: ${theme.typography.fontWeightMedium}; 18 | padding: ${theme.spacing(0.5, 1)}; 19 | color: ${theme.colors.error.contrastText}; 20 | background: ${theme.colors.error.main}; 21 | border-radius: ${theme.shape.borderRadius()}; 22 | position: relative; 23 | display: inline-block; 24 | 25 | a { 26 | color: ${theme.colors.error.contrastText}; 27 | text-decoration: underline; 28 | } 29 | 30 | a:hover { 31 | text-decoration: none; 32 | } 33 | `; 34 | 35 | return { 36 | vertical: css` 37 | ${baseStyle} 38 | margin: ${theme.spacing(0.5, 0, 0, 0)}; 39 | 40 | &:before { 41 | content: ''; 42 | position: absolute; 43 | left: 9px; 44 | top: -5px; 45 | width: 0; 46 | height: 0; 47 | border-width: 0 4px 5px 4px; 48 | border-color: transparent transparent ${theme.colors.error.main} transparent; 49 | border-style: solid; 50 | } 51 | `, 52 | horizontal: css` 53 | ${baseStyle} 54 | margin-left: 10px; 55 | 56 | &:before { 57 | content: ''; 58 | position: absolute; 59 | left: -5px; 60 | top: 9px; 61 | width: 0; 62 | height: 0; 63 | border-width: 4px 5px 4px 0; 64 | border-color: transparent #e02f44 transparent transparent; 65 | border-style: solid; 66 | } 67 | `, 68 | fieldValidationMessageIcon: css` 69 | margin-right: ${theme.spacing()}; 70 | `, 71 | }; 72 | }); 73 | 74 | class FieldValidationMessage extends Component { 75 | render() { 76 | const { children, horizontal, className, theme } = this.props; 77 | const styles = getFieldValidationMessageStyles(theme); 78 | const cssName = cx(horizontal ? styles.horizontal : styles.vertical, className); 79 | 80 | return ( 81 |
82 | 83 | {children} 84 |
85 | ); 86 | } 87 | } 88 | 89 | export default withTheme2(FieldValidationMessage); 90 | -------------------------------------------------------------------------------- /src/components/QueryEditor/InlineField.tsx: -------------------------------------------------------------------------------- 1 | import { cx, css } from '@emotion/css'; 2 | import React, { Component, HTMLAttributes } from 'react'; 3 | 4 | import { withTheme2, ReactUtils, PopoverContent, InlineLabel } from '@grafana/ui'; // Assuming you export withTheme2 from @grafana/ui 5 | import { GrafanaTheme2 } from '@grafana/data'; 6 | import FieldValidationMessage from './FieldValidationMessage'; 7 | 8 | export interface FieldProps extends HTMLAttributes { 9 | /** Form input element, i.e Input or Switch */ 10 | children: React.ReactElement; 11 | /** Label for the field */ 12 | label?: React.ReactNode; 13 | /** Description of the field */ 14 | description?: React.ReactNode; 15 | /** Indicates if field is in invalid state */ 16 | invalid?: boolean; 17 | /** Indicates if field is in loading state */ 18 | loading?: boolean; 19 | /** Indicates if field is disabled */ 20 | disabled?: boolean; 21 | /** Indicates if field is required */ 22 | required?: boolean; 23 | /** Error message to display */ 24 | error?: React.ReactNode; 25 | /** Indicates horizontal layout of the field */ 26 | horizontal?: boolean; 27 | /** make validation message overflow horizontally. Prevents pushing out adjacent inline components */ 28 | validationMessageHorizontalOverflow?: boolean; 29 | 30 | className?: string; 31 | /** 32 | * A unique id that associates the label of the Field component with the control with the unique id. 33 | * If the `htmlFor` property is missing the `htmlFor` will be inferred from the `id` or `inputId` property of the first child. 34 | * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label#attr-for 35 | */ 36 | htmlFor?: string; 37 | } 38 | 39 | export interface Props extends Omit { 40 | /** Content for the label's tooltip */ 41 | tooltip?: PopoverContent; 42 | /** Custom width for the label as a multiple of 8px */ 43 | labelWidth?: number | 'auto'; 44 | /** Make the field's child to fill the width of the row. Equivalent to setting `flex-grow:1` on the field */ 45 | grow?: boolean; 46 | /** Make the field's child shrink with width of the row. Equivalent to setting `flex-shrink:1` on the field */ 47 | shrink?: boolean; 48 | /** Make field's background transparent */ 49 | transparent?: boolean; 50 | /** Error message to display */ 51 | error?: string | null; 52 | htmlFor?: string; 53 | /** Make tooltip interactive */ 54 | interactive?: boolean; 55 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 56 | } 57 | 58 | class InlineField extends Component { 59 | static defaultProps = { 60 | labelWidth: 'auto' as any, 61 | }; 62 | 63 | getStyles = () => { 64 | const { theme, grow, shrink } = this.props; 65 | return { 66 | container: css` 67 | display: flex; 68 | flex-direction: row; 69 | align-items: flex-start; 70 | text-align: left; 71 | position: relative; 72 | flex: ${grow ? 1 : 0} ${shrink ? 1 : 0} auto; 73 | margin: 0 ${theme.spacing(0.5)} ${theme.spacing(0.5)} 0; 74 | `, 75 | childContainer: css` 76 | flex: ${grow ? 1 : 0} ${shrink ? 1 : 0} auto; 77 | `, 78 | fieldValidationWrapper: css` 79 | margin-top: ${theme.spacing(0.5)}; 80 | `, 81 | }; 82 | }; 83 | 84 | render() { 85 | const { 86 | children, 87 | label, 88 | tooltip, 89 | labelWidth, 90 | invalid, 91 | loading, 92 | disabled, 93 | className, 94 | htmlFor, 95 | grow, 96 | shrink, 97 | error, 98 | transparent, 99 | interactive, 100 | ...htmlProps 101 | } = this.props; 102 | 103 | const styles = this.getStyles(); 104 | const inputId = htmlFor ?? ReactUtils?.getChildId?.(children); 105 | 106 | const labelElement = 107 | typeof label === 'string' ? ( 108 | 115 | {label} 116 | 117 | ) : ( 118 | label 119 | ); 120 | 121 | return ( 122 |
123 | {labelElement} 124 |
125 | {React.cloneElement(children, { invalid, disabled, loading })} 126 | {invalid && error && ( 127 |
128 | {error} 129 |
130 | )} 131 |
132 |
133 | ); 134 | } 135 | } 136 | 137 | export default withTheme2(InlineField); 138 | -------------------------------------------------------------------------------- /src/components/QueryEditor/Label.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import React, { Component } from 'react'; 3 | import { withTheme2, Icon } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | 6 | export interface LabelProps extends React.LabelHTMLAttributes { 7 | children: React.ReactNode; 8 | description?: React.ReactNode; 9 | category?: React.ReactNode[]; 10 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 11 | } 12 | 13 | const getLabelStyles = (theme: GrafanaTheme2) => { 14 | return { 15 | label: css` 16 | label: Label; 17 | font-size: ${theme.typography.size.sm}; 18 | font-weight: ${theme.typography.fontWeightMedium}; 19 | line-height: 1.25; 20 | margin-bottom: ${theme.spacing(0.5)}; 21 | color: ${theme.colors.text.primary}; 22 | max-width: 480px; 23 | `, 24 | labelContent: css` 25 | display: flex; 26 | align-items: center; 27 | `, 28 | description: css` 29 | label: Label-description; 30 | color: ${theme.colors.text.secondary}; 31 | font-size: ${theme.typography.size.sm}; 32 | font-weight: ${theme.typography.fontWeightRegular}; 33 | margin-top: ${theme.spacing(0.25)}; 34 | display: block; 35 | `, 36 | categories: css` 37 | label: Label-categories; 38 | display: inline-flex; 39 | align-items: center; 40 | `, 41 | chevron: css` 42 | margin: 0 ${theme.spacing(0.25)}; 43 | `, 44 | }; 45 | }; 46 | 47 | class Label extends Component { 48 | render() { 49 | const { children, description, className, category, theme, ...labelProps } = this.props; 50 | const styles = getLabelStyles(theme); 51 | const categories = category?.map((c, i) => { 52 | return ( 53 | 54 | {c} 55 | 56 | 57 | ); 58 | }); 59 | 60 | return ( 61 |
62 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default withTheme2(Label); 75 | -------------------------------------------------------------------------------- /src/components/QueryEditor/QueryOptionGroup.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import React, { Component } from 'react'; 3 | import { getValueFormat, GrafanaTheme2 } from '@grafana/data'; 4 | import { config } from '@grafana/runtime'; 5 | import { Icon, Tooltip, withTheme2 } from '@grafana/ui'; 6 | import { Collapse } from '../Collapse'; 7 | 8 | import Stack from './Stack'; 9 | 10 | export type QueryStats = { 11 | bytes: number; 12 | // The error message displayed in the UI when we cant estimate the size of the query. 13 | message?: string; 14 | }; 15 | 16 | interface Props { 17 | title: string; 18 | collapsedInfo: string[]; 19 | queryStats?: QueryStats | null; 20 | children: React.ReactNode; 21 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 22 | } 23 | 24 | interface State { 25 | isOpen: boolean; 26 | } 27 | 28 | class QueryOptionGroup extends Component { 29 | constructor(props: Props) { 30 | super(props); 31 | this.state = { 32 | isOpen: false, 33 | }; 34 | } 35 | 36 | toggleOpen = () => { 37 | this.setState((prevState) => ({ isOpen: !prevState.isOpen })); 38 | }; 39 | 40 | getStyles = () => { 41 | const { theme } = this.props; 42 | return { 43 | collapse: css({ 44 | backgroundColor: 'unset', 45 | border: 'unset', 46 | marginBottom: 0, 47 | 48 | ['> button']: { 49 | padding: theme.spacing(0, 1), 50 | }, 51 | }), 52 | wrapper: css({ 53 | width: '100%', 54 | display: 'flex', 55 | justifyContent: 'space-between', 56 | alignItems: 'baseline', 57 | }), 58 | title: css({ 59 | flexGrow: 1, 60 | overflow: 'hidden', 61 | fontSize: theme.typography.bodySmall.fontSize, 62 | fontWeight: theme.typography.fontWeightMedium, 63 | margin: 0, 64 | }), 65 | description: css({ 66 | color: theme.colors.text.secondary, 67 | fontSize: theme.typography.bodySmall.fontSize, 68 | fontWeight: theme.typography.bodySmall.fontWeight, 69 | paddingLeft: theme.spacing(2), 70 | gap: theme.spacing(2), 71 | display: 'flex', 72 | }), 73 | body: css({ 74 | display: 'flex', 75 | gap: theme.spacing(2), 76 | flexWrap: 'wrap', 77 | }), 78 | stats: css({ 79 | margin: '0px', 80 | color: theme.colors.text.secondary, 81 | fontSize: theme.typography.bodySmall.fontSize, 82 | }), 83 | tooltip: css({ 84 | marginRight: theme.spacing(0.25), 85 | }), 86 | }; 87 | }; 88 | 89 | render() { 90 | const { title, children, collapsedInfo, queryStats } = this.props; 91 | const { isOpen } = this.state; 92 | const styles = this.getStyles(); 93 | 94 | return ( 95 |
96 | 103 |
{title}
104 | {!isOpen && ( 105 |
106 | {collapsedInfo.map((x, i) => ( 107 | {x} 108 | ))} 109 |
110 | )} 111 | 112 | } 113 | > 114 |
{children}
115 |
116 | {/**TODO: This is Loki logic that should eventually be moved to Loki */} 117 | {queryStats && config.featureToggles.lokiQuerySplitting && ( 118 | 119 | 120 | 121 | )} 122 | 123 | {queryStats &&

{generateQueryStats(queryStats)}

} 124 |
125 | ); 126 | } 127 | } 128 | 129 | export default withTheme2(QueryOptionGroup); 130 | 131 | const generateQueryStats = (queryStats: QueryStats) => { 132 | if (queryStats.message) { 133 | return queryStats.message; 134 | } 135 | 136 | return `This query will process approximately ${convertUnits(queryStats)}.`; 137 | }; 138 | 139 | const convertUnits = (queryStats: QueryStats): string => { 140 | const { text, suffix } = getValueFormat('bytes')(queryStats.bytes, 1); 141 | return text + suffix; 142 | }; 143 | -------------------------------------------------------------------------------- /src/components/QueryEditor/SecretInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button, Input } from '@grafana/ui'; 3 | import Stack from './Stack'; 4 | 5 | export type Props = React.ComponentProps & { 6 | /** TRUE if the secret was already configured. (It is needed as often the backend doesn't send back the actual secret, only the information that it was configured) */ 7 | isConfigured: boolean; 8 | /** Called when the user clicks on the "Reset" button in order to clear the secret */ 9 | onReset: () => void; 10 | }; 11 | 12 | export const CONFIGURED_TEXT = 'configured'; 13 | export const RESET_BUTTON_TEXT = 'Reset'; 14 | 15 | export const SecretInput = ({ isConfigured, onReset, ...props }: Props) => ( 16 | 17 | {!isConfigured && } 18 | {isConfigured && } 19 | {isConfigured && ( 20 | 23 | )} 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/components/QueryEditor/Space.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import React, { Component } from 'react'; 3 | import { GrafanaTheme2 } from '@grafana/data'; 4 | import { withTheme2 } from '@grafana/ui'; 5 | 6 | export interface SpaceProps { 7 | v?: number; 8 | h?: number; 9 | layout?: 'block' | 'inline'; 10 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 11 | } 12 | 13 | /** 14 | * @deprecated use the Space component from @grafana/ui instead. Available starting from @grafana/ui@10.4.0 15 | */ 16 | class Space extends Component { 17 | static defaultProps = { 18 | v: 0, 19 | h: 0, 20 | layout: 'block' as any, 21 | }; 22 | 23 | getStyles = () => { 24 | const { theme, v, h, layout } = this.props; 25 | return { 26 | wrapper: css([ 27 | { 28 | paddingRight: theme.spacing(h ?? 0), 29 | paddingBottom: theme.spacing(v ?? 0), 30 | }, 31 | layout === 'inline' && { 32 | display: 'inline-block', 33 | }, 34 | layout === 'block' && { 35 | display: 'block', 36 | }, 37 | ]), 38 | }; 39 | }; 40 | 41 | render() { 42 | const styles = this.getStyles(); 43 | return ; 44 | } 45 | } 46 | 47 | export default withTheme2(Space); 48 | -------------------------------------------------------------------------------- /src/components/QueryEditor/Stack.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import React, { Component, CSSProperties } from 'react'; 3 | import { withTheme2 } from '@grafana/ui'; 4 | import {GrafanaTheme2} from '@grafana/data' 5 | 6 | interface StackProps { 7 | direction?: CSSProperties['flexDirection']; 8 | alignItems?: CSSProperties['alignItems']; 9 | wrap?: boolean; 10 | gap?: number; 11 | flexGrow?: CSSProperties['flexGrow']; 12 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 13 | } 14 | 15 | /** 16 | * @deprecated use the Stack component from @grafana/ui instead. Available starting from @grafana/ui@10.2.3 17 | */ 18 | class Stack extends Component> { 19 | static defaultProps = { 20 | direction: 'row' as any, 21 | wrap: true, 22 | gap: 2, 23 | }; 24 | 25 | getStyles = () => { 26 | const { theme, direction = 'row', wrap = true, alignItems, gap = 2, flexGrow } = this.props; 27 | return { 28 | root: css({ 29 | display: 'flex', 30 | flexDirection: direction, 31 | flexWrap: wrap ? 'wrap' : undefined, 32 | alignItems: alignItems, 33 | gap: theme.spacing(gap), 34 | flexGrow: flexGrow, 35 | width: '100%' 36 | }), 37 | }; 38 | }; 39 | 40 | render() { 41 | const styles = this.getStyles(); 42 | const { children } = this.props; 43 | 44 | return
{children}
; 45 | } 46 | } 47 | 48 | export default withTheme2(Stack); 49 | 50 | -------------------------------------------------------------------------------- /src/components/RadioButtonGroup/RadioButton.tsx: -------------------------------------------------------------------------------- 1 | import { css, CSSObject } from '@emotion/css'; 2 | import React, { Component } from 'react'; 3 | import { withTheme2 } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | 6 | import { StringSelector } from '@grafana/e2e-selectors'; 7 | 8 | interface RadioButtonProps { 9 | size?: RadioButtonSize; 10 | disabled?: boolean; 11 | name?: string; 12 | description?: string; 13 | active: boolean; 14 | id: string; 15 | onChange: () => void; 16 | onClick: () => void; 17 | fullWidth?: boolean; 18 | 'aria-label'?: StringSelector; 19 | children?: React.ReactNode; 20 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 21 | inputRef?: React.RefObject; 22 | } 23 | 24 | export type RadioButtonSize = 'sm' | 'md'; 25 | 26 | class RadioButton_ extends Component { 27 | // inputRef = React.createRef(); 28 | 29 | getStyles = () => { 30 | const { theme, size, fullWidth } = this.props; 31 | const { fontSize, height, padding } = getPropertiesForButtonSize(size || 'md', theme); 32 | 33 | const textColor = theme.colors.text.secondary; 34 | const textColorHover = theme.colors.text.primary; 35 | // remove the group inner padding (set on RadioButtonGroup) 36 | const labelHeight = height * theme.spacing.gridSize - 4 - 2; 37 | 38 | return { 39 | radio: css` 40 | position: absolute; 41 | opacity: 0; 42 | z-index: -1000; 43 | 44 | &:checked + label { 45 | color: ${theme.colors.text.primary}; 46 | font-weight: ${theme.typography.fontWeightMedium}; 47 | background: ${theme.colors.action.selected}; 48 | z-index: 3; 49 | } 50 | 51 | &:focus + label, 52 | &:focus-visible + label { 53 | ${getFocusStyles(theme)}; 54 | } 55 | 56 | &:focus:not(:focus-visible) + label { 57 | ${getMouseFocusStyles(theme)} 58 | } 59 | 60 | &:disabled + label { 61 | color: ${theme.colors.text.disabled}; 62 | cursor: not-allowed; 63 | } 64 | `, 65 | radioLabel: css` 66 | display: inline-block; 67 | position: relative; 68 | font-size: ${fontSize}; 69 | height: ${labelHeight}px; 70 | // Deduct border from line-height for perfect vertical centering on windows and linux 71 | line-height: ${labelHeight}px; 72 | color: ${textColor}; 73 | padding: ${theme.spacing(0, padding)}; 74 | border-radius: ${theme.shape.borderRadius()}; 75 | background: ${theme.colors.background.primary}; 76 | cursor: pointer; 77 | z-index: 1; 78 | flex: ${fullWidth ? `1 0 0` : 'none'}; 79 | text-align: center; 80 | user-select: none; 81 | white-space: nowrap; 82 | 83 | &:hover { 84 | color: ${textColorHover}; 85 | } 86 | `, 87 | }; 88 | }; 89 | 90 | render() { 91 | const { 92 | children, 93 | active, 94 | disabled, 95 | onChange, 96 | onClick, 97 | id, 98 | name, 99 | description, 100 | inputRef, 101 | 'aria-label': ariaLabel, 102 | } = this.props; 103 | 104 | const styles = this.getStyles(); 105 | 106 | return ( 107 | <> 108 | 120 | 123 | 124 | ); 125 | } 126 | } 127 | 128 | export const RadioButton = withTheme2(RadioButton_); 129 | 130 | // Utility functions 131 | 132 | export function getMouseFocusStyles(theme: GrafanaTheme2): CSSObject { 133 | return { 134 | outline: 'none', 135 | boxShadow: `none`, 136 | }; 137 | } 138 | 139 | export function getFocusStyles(theme: GrafanaTheme2): CSSObject { 140 | return { 141 | outline: '2px dotted transparent', 142 | outlineOffset: '2px', 143 | boxShadow: `0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main}`, 144 | transitionTimingFunction: `cubic-bezier(0.19, 1, 0.22, 1)`, 145 | transitionDuration: '0.2s', 146 | transitionProperty: 'outline, outline-offset, box-shadow', 147 | }; 148 | } 149 | 150 | function getPropertiesForButtonSize(size: any, theme: GrafanaTheme2) { 151 | switch (size) { 152 | case 'sm': 153 | return { 154 | padding: 1, 155 | fontSize: theme.typography.size.sm, 156 | height: theme.components.height.sm, 157 | }; 158 | 159 | case 'lg': 160 | return { 161 | padding: 3, 162 | fontSize: theme.typography.size.lg, 163 | height: theme.components.height.lg, 164 | }; 165 | case 'md': 166 | default: 167 | return { 168 | padding: 2, 169 | fontSize: theme.typography.size.md, 170 | height: theme.components.height.md, 171 | }; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/components/RadioButtonGroup/RadioButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import { uniqueId } from 'lodash'; 3 | import React, { Component, createRef } from 'react'; 4 | 5 | import { GrafanaTheme2, SelectableValue } from '@grafana/data'; 6 | import { Icon, withTheme2 } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui 7 | 8 | import { RadioButtonSize, RadioButton } from './RadioButton'; 9 | 10 | interface RadioButtonGroupProps { 11 | value?: T; 12 | id?: string; 13 | disabled?: boolean; 14 | disabledOptions?: T[]; 15 | options: Array>; 16 | onChange?: (value: T) => void; 17 | onClick?: (value: T) => void; 18 | size?: RadioButtonSize; 19 | fullWidth?: boolean; 20 | className?: string; 21 | autoFocus?: boolean; 22 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC 23 | } 24 | 25 | class RadioButtonGroup extends Component> { 26 | static defaultProps = { 27 | size: 'md' as any, 28 | fullWidth: false, 29 | autoFocus: false, 30 | }; 31 | 32 | activeButtonRef = createRef(); 33 | 34 | handleOnChange = (option: SelectableValue) => { 35 | return () => { 36 | if (this.props.onChange) { 37 | this.props.onChange(option.value); 38 | } 39 | }; 40 | }; 41 | 42 | handleOnClick = (option: SelectableValue) => { 43 | return () => { 44 | if (this.props.onClick) { 45 | this.props.onClick(option.value); 46 | } 47 | }; 48 | }; 49 | 50 | componentDidMount() { 51 | if (this.props.autoFocus && this.activeButtonRef.current) { 52 | this.activeButtonRef.current.focus(); 53 | } 54 | } 55 | 56 | getStyles = () => { 57 | const { theme } = this.props; 58 | return { 59 | radioGroup: css({ 60 | display: 'inline-flex', 61 | flexDirection: 'row', 62 | flexWrap: 'nowrap', 63 | border: `1px solid ${theme.components.input.borderColor}`, 64 | borderRadius: theme.shape.borderRadius(), 65 | padding: '2px', 66 | }), 67 | fullWidth: css({ 68 | display: 'flex', 69 | }), 70 | icon: css` 71 | margin-right: 6px; 72 | `, 73 | img: css` 74 | width: ${theme.spacing(2)}; 75 | height: ${theme.spacing(2)}; 76 | margin-right: ${theme.spacing(1)}; 77 | `, 78 | }; 79 | }; 80 | 81 | render() { 82 | const { 83 | options, 84 | value, 85 | disabled, 86 | disabledOptions, 87 | size, 88 | id, 89 | className, 90 | fullWidth, 91 | } = this.props; 92 | 93 | const internalId = id ?? uniqueId('radiogroup-'); 94 | const groupName = internalId; 95 | const styles = this.getStyles(); 96 | 97 | return ( 98 |
99 | {options.map((o, i) => { 100 | const isItemDisabled = disabledOptions && o.value && disabledOptions.includes(o.value); 101 | return ( 102 | 116 | {o.icon && } 117 | {o.imgUrl && {o.label}} 118 | {o.label} {o.component ? : null} 119 | 120 | ); 121 | })} 122 |
123 | ); 124 | } 125 | } 126 | 127 | export default withTheme2(RadioButtonGroup); 128 | -------------------------------------------------------------------------------- /src/components/Switch/switch.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLProps, useRef } from 'react'; 2 | import { css, cx } from '@emotion/css'; 3 | import { uniqueId } from 'lodash'; 4 | import { GrafanaTheme2, deprecationWarning } from '@grafana/data'; 5 | import { stylesFactory, useTheme2 } from '@grafana/ui'; 6 | 7 | import { getFocusStyles, getMouseFocusStyles } from '../RadioButtonGroup/RadioButton'; 8 | 9 | export interface Props extends Omit, 'value'> { 10 | value?: boolean; 11 | /** Make switch's background and border transparent */ 12 | transparent?: boolean; 13 | } 14 | 15 | export const Switch = React.forwardRef( 16 | ({ value, checked, disabled, onChange, id, ...inputProps }, ref) => { 17 | if (checked) { 18 | deprecationWarning('Switch', 'checked prop', 'value'); 19 | } 20 | 21 | const theme = useTheme2(); 22 | const styles = getSwitchStyles(theme); 23 | const switchIdRef = useRef(id ? id : uniqueId('switch-')); 24 | 25 | return ( 26 |
27 | { 32 | console.log(event) 33 | onChange?.(event); 34 | }} 35 | id={switchIdRef.current} 36 | {...inputProps} 37 | ref={ref} 38 | /> 39 |
41 | ); 42 | } 43 | ); 44 | 45 | Switch.displayName = 'Switch'; 46 | 47 | export interface InlineSwitchProps extends Props { 48 | showLabel?: boolean; 49 | } 50 | 51 | export const InlineSwitch = React.forwardRef( 52 | ({ transparent, showLabel, label, value, id, onChange, ...props }, ref) => { 53 | const theme = useTheme2(); 54 | const styles = getSwitchStyles(theme, transparent); 55 | console.log(value) 56 | 57 | return ( 58 |
59 | {showLabel && ( 60 | 66 | )} 67 | 68 |
69 | ); 70 | } 71 | ); 72 | 73 | InlineSwitch.displayName = 'Switch'; 74 | 75 | const getSwitchStyles = stylesFactory((theme: GrafanaTheme2, transparent?: boolean) => { 76 | return { 77 | switch: css` 78 | width: 32px; 79 | height: 16px; 80 | position: relative; 81 | 82 | input { 83 | opacity: 0; 84 | left: -100vw; 85 | z-index: -1000; 86 | position: absolute; 87 | 88 | &:disabled + label { 89 | background: ${theme.colors.action.disabledBackground}; 90 | cursor: not-allowed; 91 | } 92 | 93 | &:checked + label { 94 | background: ${theme.colors.primary.main}; 95 | border-color: ${theme.colors.primary.main}; 96 | 97 | &:hover { 98 | background: ${theme.colors.primary.shade}; 99 | } 100 | 101 | &::after { 102 | transform: translate3d(18px, -50%, 0); 103 | background: ${theme.colors.primary.contrastText}; 104 | } 105 | } 106 | 107 | &:focus + label, 108 | &:focus-visible + label { 109 | ${getFocusStyles(theme)} 110 | } 111 | 112 | &:focus:not(:focus-visible) + label { 113 | ${getMouseFocusStyles(theme)} 114 | } 115 | } 116 | 117 | label { 118 | width: 100%; 119 | height: 100%; 120 | cursor: pointer; 121 | border: none; 122 | border-radius: 50px; 123 | background: ${theme.components.input.background}; 124 | border: 1px solid ${theme.components.input.borderColor}; 125 | transition: all 0.3s ease; 126 | position: absolute; 127 | 128 | &:hover { 129 | border-color: ${theme.components.input.borderHover}; 130 | } 131 | 132 | &::after { 133 | position: absolute; 134 | display: block; 135 | content: ''; 136 | width: 12px; 137 | height: 12px; 138 | border-radius: 6px; 139 | background: ${theme.colors.text.secondary}; 140 | box-shadow: ${theme.shadows.z1}; 141 | top: 50%; 142 | transform: translate3d(2px, -50%, 0); 143 | transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1); 144 | } 145 | } 146 | `, 147 | inlineContainer: css` 148 | padding: ${theme.spacing(0, 1)}; 149 | height: ${theme.spacing(theme.components.height.md)}; 150 | display: inline-flex; 151 | align-items: center; 152 | background: ${transparent ? 'transparent' : theme.components.input.background}; 153 | border: 1px solid ${transparent ? 'transparent' : theme.components.input.borderColor}; 154 | border-radius: ${theme.shape.borderRadius()}; 155 | 156 | &:hover { 157 | border: 1px solid ${transparent ? 'transparent' : theme.components.input.borderHover}; 158 | 159 | .inline-switch-label { 160 | color: ${theme.colors.text.primary}; 161 | } 162 | } 163 | `, 164 | inlineLabel: css` 165 | cursor: pointer; 166 | padding-right: ${theme.spacing(1)}; 167 | color: ${theme.colors.text.secondary}; 168 | white-space: nowrap; 169 | `, 170 | inlineLabelEnabled: css` 171 | color: ${theme.colors.text.primary}; 172 | `, 173 | }; 174 | }); 175 | -------------------------------------------------------------------------------- /src/components/configSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, PureComponent } from 'react'; 2 | import { IconName } from '@grafana/ui'; 3 | import { IconButton } from './IconButton'; 4 | import './style.css'; 5 | 6 | export type Props = { 7 | title: string; 8 | description?: ReactNode; 9 | isCollapsible?: boolean; 10 | isInitiallyOpen?: boolean; 11 | kind?: 'section' | 'sub-section'; 12 | className?: string; 13 | children: ReactNode; 14 | }; 15 | 16 | export class ConfigSection extends PureComponent { 17 | state = { 18 | isOpen: true, 19 | }; 20 | render() { 21 | const { children, title, description, isCollapsible = false, kind = 'section' } = this.props; 22 | const { isOpen } = this.state; 23 | const iconName: IconName = isOpen ? 'angle-up' : 'angle-down'; 24 | const isSubSection = kind === 'sub-section'; 25 | const collapsibleButtonAriaLabel = `${isOpen ? 'Collapse' : 'Expand'} section ${title}`; 26 | 27 | return ( 28 |
29 |
30 | {kind === 'section' ?

{title}

:
{title}
} 31 | {isCollapsible && ( 32 | this.setState({ isOpen: !isOpen })} 35 | type="button" 36 | size="xl" 37 | aria-label={collapsibleButtonAriaLabel} 38 | /> 39 | )} 40 |
41 | {description && ( 42 |

50 | {description} 51 |

52 | )} 53 | {isOpen &&
{children}
} 54 |
55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/description.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import './style.css'; 3 | 4 | type Props = { 5 | dataSourceName: string; 6 | docsLink: string; 7 | hasRequiredFields?: boolean; 8 | className?: string; 9 | }; 10 | 11 | export class DataSourceDescription extends PureComponent { 12 | render() { 13 | const { dataSourceName, docsLink, hasRequiredFields = true } = this.props; 14 | 15 | return ( 16 |
17 |

18 | Before you can use the {dataSourceName} data source, you must configure it below or in the config file. For 19 | detailed instructions, 20 | 21 | view the documentation 22 | 23 | . 24 |

25 | {hasRequiredFields && ( 26 |

27 | Fields marked with * are required 28 |

29 | )} 30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/style.css: -------------------------------------------------------------------------------- 1 | 2 | .description-container { 3 | p{ 4 | margin: 0; 5 | } 6 | 7 | p+p{ 8 | margin-top: 16px; 9 | } 10 | 11 | .description-text { 12 | font-weight: 400; 13 | font-family: Inter, Helvetica, Arial, sans-serif; 14 | } 15 | 16 | a { 17 | color: #6E9FFF; 18 | text-decoration: underline; 19 | } 20 | a::hover { 21 | text-decoration: none; 22 | } 23 | } 24 | 25 | .config-section { 26 | .header { 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; 30 | } 31 | .title { 32 | margin: 0, 33 | } 34 | 35 | .subtitle { 36 | margin: 0; 37 | font-weight: 400 38 | } 39 | } 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | export const xColInfoSeries = [ 2 | { 3 | color: '#ff006e', 4 | label: '空 empty', 5 | value: '表格或日志 table or log', 6 | }, 7 | { 8 | color: '#80ed99', 9 | label: '【时间列 Time colomn】', 10 | value: '时序数据 Timeseries', 11 | }, 12 | { 13 | color: '#0077b6', 14 | label: 'stat', 15 | value: '单值图格式 gauge/stat graph', 16 | }, 17 | { 18 | color: '#0096c7', 19 | label: 'pie', 20 | value: '饼图格式 pie graph', 21 | }, 22 | { 23 | color: '#00b4d8', 24 | label: 'bar', 25 | value: '柱状图格式 bar graph', 26 | }, 27 | { 28 | color: '#48cae4', 29 | label: 'trace', 30 | value: 'Trace格式 Trace graph', 31 | }, 32 | { 33 | color: '#90e0ef', 34 | label: 'map', 35 | value: '地图格式 map graph', 36 | }, 37 | ]; 38 | 39 | export const yColInfoSeries = [ 40 | { 41 | color: '#ffca3a', 42 | label: '【无需填写】 此情况适用于Log、Trace以及表格全列展示的情况', 43 | value: '[No need to fill in] This situation applies to Log, Trace and the full column of the table', 44 | }, 45 | { 46 | color: '#8ac926', 47 | label: '【col1,col2,col3】 最常用的一种形式,在图表中展示若干数值列', 48 | value: '[col1,col2,col3] The most common form, displaying several numeric columns in the chart', 49 | }, 50 | { 51 | color: '#1982c4', 52 | label: '【col1#:#col2】 适用于bar和SLS时序库展示,col1为聚合列,col2为其他列', 53 | value: '[col1#:#col2] Applicable to bar and SLS metricStore, col1 is the aggregation column, col2 is other columns', 54 | }, 55 | ]; 56 | 57 | export const totalLogsInfoSeries = [ 58 | { 59 | color: '#ffca3a', 60 | label: '只对 Query 查询语句生效,分析语句无效', 61 | value: 'Only effective for search statements, not applicable to analytic statements', 62 | }, 63 | { 64 | color: '#8ac926', 65 | label: '最小值为 1 条', 66 | value: 'Minimum value is 1', 67 | }, 68 | { 69 | color: '#1982c4', 70 | label: '最大值为 5000 条', 71 | value: 'Maximum value is 5000', 72 | }, 73 | ]; 74 | 75 | export const xSelectOptions = [ 76 | { 77 | label: 'TimeSeries / Custom', 78 | value: 'custom', 79 | description: '时序数据 Timeseries 自定义【时间列 Time colomn】,或自定义x轴输入', 80 | }, 81 | { label: 'Table / Log', value: '', description: '表格或日志 table or log' }, 82 | { label: 'Stat / Gauge', value: 'stat', description: '单值图格式 gauge/stat graph' }, 83 | { label: 'Pie', value: 'pie', description: '饼图格式 pie graph' }, 84 | { label: 'Bar', value: 'bar', description: '柱状图格式 bar graph' }, 85 | { label: 'Trace', value: 'trace', description: 'Trace格式 Trace graph' }, 86 | { label: 'Map', value: 'map', description: '地图格式 map graph' }, 87 | // { 88 | // label: 'Option with description and image', 89 | // value: 2, 90 | // description: 'This is a very elaborate description, describing all the wonders in the world.', 91 | // imgUrl: 'https://placekitten.com/40/40', 92 | // }, 93 | ]; 94 | 95 | export const dataSourceType = [ 96 | { label: 'ALL(SQL)', value: 'all' }, 97 | { 98 | label: 'Logstore(SQL)', 99 | value: 'logstore', 100 | }, 101 | { label: 'Metricstore(SQL)', value: 'metricsql' }, 102 | { label: 'Metricstore(PromQL)', value: 'metricstore' }, 103 | ]; 104 | 105 | export const version = (window as any)?.grafanaBootData?.settings?.buildInfo?.version ?? ''; 106 | -------------------------------------------------------------------------------- /src/custom-header/custom-header.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { css, cx } from '@emotion/css'; 3 | import { InlineFieldRow, InlineField, Input, IconButton, withTheme2 } from '@grafana/ui'; 4 | import type { LocalHeader } from '../types'; 5 | 6 | export type Props = { 7 | header: LocalHeader; 8 | onChange: (header: LocalHeader) => void; 9 | onBlur: () => void; 10 | onDelete: () => void; 11 | readOnly: boolean; 12 | theme: any; // Add theme to props for withTheme2 HOC 13 | }; 14 | 15 | const getCommonStyles = () => { 16 | return { 17 | inlineFieldNoMarginRight: css({ 18 | marginRight: 0, 19 | }), 20 | // This is dirty hack to make configured secret input grow 21 | inlineFieldWithSecret: css({ 22 | '[class$="layoutChildrenWrapper"]:first-child': { 23 | flexGrow: 1, 24 | }, 25 | }), 26 | }; 27 | }; 28 | 29 | class CustomHeader extends Component { 30 | handleInputChange = (field: 'name' | 'value') => (event: React.ChangeEvent) => { 31 | const { header, onChange } = this.props; 32 | onChange({ ...header, [field]: event.currentTarget.value }); 33 | }; 34 | 35 | handleReset = () => { 36 | const { header, onChange } = this.props; 37 | onChange({ ...header, configured: false, value: '' }); 38 | }; 39 | 40 | render() { 41 | const { header, onBlur, onDelete, readOnly, theme } = this.props; 42 | const { spacing } = theme; 43 | const commonStyles = getCommonStyles(); 44 | 45 | const styles = { 46 | container: css({ 47 | alignItems: 'center', 48 | }), 49 | headerNameField: css({ 50 | width: '40%', 51 | marginRight: 0, 52 | paddingRight: spacing(1), 53 | }), 54 | headerValueField: css({ 55 | width: '45%', 56 | marginRight: 0, 57 | }), 58 | removeHeaderBtn: css({ 59 | margin: `0 0 3px 10px`, 60 | }), 61 | }; 62 | 63 | return ( 64 | 65 | 73 | 80 | 81 | 89 | {} : this.handleReset} 95 | onBlur={onBlur} 96 | /> 97 | 98 | 107 | 108 | ); 109 | } 110 | } 111 | 112 | export default withTheme2(CustomHeader); 113 | -------------------------------------------------------------------------------- /src/custom-header/custom-headers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { Button, withTheme2 } from '@grafana/ui'; 4 | import CustomHeader from './custom-header'; 5 | import type { HeaderWithValue, LocalHeader } from '../types'; 6 | import { ConfigSection } from '../components/configSection'; 7 | 8 | export type Props = { 9 | headers: HeaderWithValue[]; 10 | onChange: (headers: HeaderWithValue[]) => void; 11 | readOnly: boolean; 12 | theme: any; // Add theme to props for withTheme2 HOC 13 | }; 14 | 15 | type State = { 16 | headers: LocalHeader[]; 17 | }; 18 | 19 | class CustomHeaders extends Component { 20 | constructor(props: Props) { 21 | super(props); 22 | this.state = { 23 | headers: props.headers.map((header) => ({ 24 | ...header, 25 | id: uniqueId(), 26 | })), 27 | }; 28 | } 29 | 30 | componentDidUpdate(prevProps: Props) { 31 | if (this.props.headers !== prevProps.headers) { 32 | this.setState((prevState) => { 33 | let changed = false; 34 | const newHeaders = prevState.headers.map((header) => { 35 | const configured = this.props.headers.find((h) => h.name === header.name)?.configured; 36 | if (typeof configured !== 'undefined' && header.configured !== configured) { 37 | changed = true; 38 | return { ...header, configured }; 39 | } 40 | return header; 41 | }); 42 | 43 | if (changed) { 44 | return { headers: newHeaders }; 45 | } 46 | 47 | return prevState; 48 | }); 49 | } 50 | } 51 | 52 | onHeaderAdd = (e: React.MouseEvent) => { 53 | e.preventDefault() 54 | this.setState( 55 | (prevState) => ({ 56 | headers: [...prevState.headers, { id: uniqueId(), name: '', value: '', configured: false }], 57 | }), 58 | this.triggerChange 59 | ); 60 | }; 61 | 62 | onHeaderChange = (id: string, header: LocalHeader) => { 63 | this.setState( 64 | (prevState) => ({ 65 | headers: prevState.headers.map((h) => (h.id === id ? { ...header } : h)), 66 | }), 67 | this.triggerChange 68 | ); 69 | }; 70 | 71 | onHeaderDelete = (id: string) => { 72 | this.setState( 73 | (prevState) => { 74 | const index = prevState.headers.findIndex((h) => h.id === id); 75 | if (index === -1) { 76 | return prevState; 77 | } 78 | const newHeaders = [...prevState.headers]; 79 | newHeaders.splice(index, 1); 80 | return { headers: newHeaders }; 81 | }, 82 | this.triggerChange 83 | ); 84 | }; 85 | 86 | onBlur = () => { 87 | this.triggerChange(); 88 | }; 89 | 90 | triggerChange = () => { 91 | const { headers } = this.state; 92 | this.props.onChange( 93 | headers.map(({ name, value, configured }) => ({ 94 | name, 95 | value, 96 | configured, 97 | })) 98 | ); 99 | }; 100 | 101 | render() { 102 | const { readOnly, theme } = this.props; 103 | const { headers } = this.state; 104 | const { spacing } = theme; 105 | 106 | const styles = { 107 | container: css({ 108 | marginTop: spacing(3), 109 | }), 110 | addHeaderButton: css({ 111 | marginTop: spacing(1.5), 112 | }), 113 | }; 114 | 115 | return ( 116 |
117 | 124 |
125 | {headers.map((header) => ( 126 | this.onHeaderChange(header.id, header)} 130 | onDelete={() => this.onHeaderDelete(header.id)} 131 | onBlur={this.onBlur} 132 | readOnly={readOnly} 133 | /> 134 | ))} 135 |
136 |
137 | 140 |
141 |
142 |
143 | ); 144 | } 145 | } 146 | 147 | export default withTheme2(CustomHeaders); 148 | 149 | function uniqueId(): string { 150 | return Math.random().toString(16).slice(2); 151 | } 152 | -------------------------------------------------------------------------------- /src/datasource.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TraceLog, 3 | TraceKeyValuePair, 4 | TraceSpanRow, 5 | DataFrame, 6 | DataQueryRequest, 7 | DataQueryResponse, 8 | DataSourceInstanceSettings, 9 | FieldType, 10 | MutableDataFrame, 11 | Vector, 12 | MetricFindValue, 13 | } from '@grafana/data'; 14 | import { defaultQuery, SLSDataSourceOptions, SLSQuery } from './types'; 15 | import { DataSourceWithBackend, getBackendSrv, getTemplateSrv } from '@grafana/runtime'; 16 | import _ from 'lodash'; 17 | import { map } from 'rxjs/operators'; 18 | 19 | export class SLSDataSource extends DataSourceWithBackend { 20 | constructor(instanceSettings: DataSourceInstanceSettings) { 21 | super(instanceSettings); 22 | } 23 | 24 | query(options: DataQueryRequest) { 25 | // console.log("into query.") 26 | options.targets.forEach((q: SLSQuery) => { 27 | q.query = replaceQueryParameters(q, options); 28 | if(/__custom__@/.test(q?.logstore ?? '')){ 29 | const currentLogstore: any = getTemplateSrv().getVariables().find((item) => item.name === q?.logstore?.split('@')[1]); 30 | const value = currentLogstore?.current?.value; 31 | q.logstore = value; 32 | } 33 | }); 34 | if (options.targets[0].xcol === 'trace') { 35 | return super.query(options).pipe(map(responseToDataQueryResponse)); 36 | } 37 | return super.query(options); 38 | } 39 | 40 | metricFindQuery(query: SLSQuery | string, options?: any): Promise { 41 | const _defaultQuery = { ...defaultQuery, ...(typeof query === 'string' ? { query } : query)} 42 | const { type, logstore, queryType, legendFormat, step } = _defaultQuery; 43 | const data = { 44 | from: options.range.from.valueOf().toString(), 45 | to: options.range.to.valueOf().toString(), 46 | queries: [ 47 | { 48 | // datasource: this.name, 49 | datasource: { type: this.type, uid: this.uid }, 50 | datasourceId: this.id, 51 | query: replaceQueryParameters(_defaultQuery.query || '', options), 52 | type, 53 | logstore, 54 | queryType, 55 | legendFormat, 56 | step, 57 | }, 58 | ], 59 | }; 60 | 61 | return new Promise(async (resolve) => { 62 | let values: any[] = []; 63 | 64 | try { 65 | const response = await getBackendSrv().post('/api/ds/query', data); 66 | values = response.results.A.frames ?? [] 67 | } catch (error) { 68 | console.log('error', error) 69 | } 70 | const res = mapToTextValueNew(values); 71 | resolve(res); 72 | }); 73 | } 74 | } 75 | 76 | function valueToTag(key: any, value: any): TraceKeyValuePair { 77 | return { 78 | key, 79 | value, 80 | }; 81 | } 82 | 83 | function transServiceTags(result: Map, i: number): TraceKeyValuePair[] { 84 | const resource = JSON.parse(result.get('resource')?.get(i)); 85 | const resourceArray = Array.from(Object.entries(resource), ([name, value]) => valueToTag(name, value)); 86 | resourceArray.push(valueToTag('host', result.get('host')?.get(i))); 87 | return resourceArray; 88 | } 89 | 90 | function transTags(result: Map, i: number): TraceKeyValuePair[] { 91 | const attribute = JSON.parse(result.get('attribute')?.get(i)); 92 | const resourceArray = Array.from(Object.entries(attribute), ([name, value]) => valueToTag(name, value)); 93 | resourceArray.push(valueToTag('statusCode', result.get('statusCode')?.get(i))); 94 | resourceArray.push(valueToTag('statusMessage', result.get('statusMessage')?.get(i))); 95 | return resourceArray; 96 | } 97 | 98 | function transLogs(result: Map, i: number): TraceLog[] { 99 | let traceLogs: TraceLog[] = []; 100 | const slsLogs = JSON.parse(result.get('logs')?.get(i)) as Object[]; 101 | for (const slsLog of slsLogs) { 102 | const logMap = new Map(Object.entries(slsLog)); 103 | const attributeArray = Array.from(Object.entries(slsLog), ([name, value]) => valueToTag(name, value)); 104 | let time = logMap.get('time'); 105 | if (time !== undefined) { 106 | traceLogs.push({ 107 | timestamp: Number(time) / 1000000, 108 | fields: attributeArray, 109 | }); 110 | } 111 | } 112 | return traceLogs; 113 | } 114 | 115 | function transformSpan(df: DataFrame): TraceSpanRow[] { 116 | let traceSpanRows: TraceSpanRow[] = []; 117 | const fields = df.fields; 118 | const result = new Map(fields.map((key) => [key.name, key.values])); 119 | for (let i = 0; i < df.length; i++) { 120 | const tsd = { 121 | traceID: result.get('traceID')?.get(i), 122 | spanID: result.get('spanID')?.get(i), 123 | parentSpanID: result.get('parentSpanID')?.get(i), 124 | operationName: result.get('operationName')?.get(i), 125 | serviceName: result.get('serviceName')?.get(i), 126 | serviceTags: transServiceTags(result, i), 127 | startTime: result.get('startTime')?.get(i), 128 | duration: result.get('duration')?.get(i), 129 | tags: transTags(result, i), 130 | errorIconColor: result.get('statusCode')?.get(i) === 'ERROR' ? '#f00' : '', 131 | logs: transLogs(result, i), 132 | }; 133 | traceSpanRows.push(tsd); 134 | } 135 | return traceSpanRows; 136 | } 137 | 138 | export function transformResponse(df: DataFrame): DataFrame { 139 | const spanRows = transformSpan(df); 140 | const frame = new MutableDataFrame({ 141 | fields: [ 142 | { name: 'traceID', type: FieldType.string }, 143 | { name: 'spanID', type: FieldType.string }, 144 | { name: 'parentSpanID', type: FieldType.string }, 145 | { name: 'operationName', type: FieldType.string }, 146 | { name: 'serviceName', type: FieldType.string }, 147 | { name: 'serviceTags', type: FieldType.other }, 148 | { name: 'startTime', type: FieldType.number }, 149 | { name: 'duration', type: FieldType.number }, 150 | { name: 'logs', type: FieldType.other }, 151 | { name: 'tags', type: FieldType.other }, 152 | { name: 'errorIconColor', type: FieldType.string }, 153 | ], 154 | meta: { 155 | preferredVisualisationType: 'trace', 156 | }, 157 | }); 158 | 159 | for (const span of spanRows) { 160 | frame.add(span); 161 | } 162 | 163 | return frame; 164 | } 165 | 166 | function responseToDataQueryResponse(response: DataQueryResponse): DataQueryResponse { 167 | return { 168 | data: [transformResponse(response.data[0])], 169 | }; 170 | } 171 | 172 | export function mapToTextValue(result: any) { 173 | if (Array.isArray(result) && result.length === 2) { 174 | return _.map(result[0], (d, i) => { 175 | return { text: d, value: result[1][i] }; 176 | }); 177 | } 178 | return _.map(result[0], (d, i) => { 179 | if (d && d.text && d.value) { 180 | return { text: d.text, value: d.value }; 181 | } else if (_.isObject(d)) { 182 | return { text: d, value: i }; 183 | } 184 | return { text: d, value: d }; 185 | }); 186 | } 187 | 188 | export function mapToTextValueNew(frames: any): MetricFindValue[] { 189 | 190 | if(!frames || !Array.isArray(frames) || frames?.length === 0 ) { 191 | return [] 192 | } 193 | 194 | const TextValues: MetricFindValue[] = []; 195 | for(const frame of frames) { 196 | if(frame.data?.values?.length > 0) { 197 | const fieldsValues = frame.data.values 198 | // 如果只有两列 默认第一列为text 199 | if(fieldsValues.length === 2) { 200 | for(let i = 0; i < fieldsValues[0].length; i++) { 201 | TextValues.push({ text: fieldsValues[0][i], value: fieldsValues[1][i] }); 202 | } 203 | } else { 204 | for(let rowIndx = 0; rowIndx < fieldsValues[0].length; rowIndx++) { 205 | 206 | // 保存之前的逻辑 207 | const columnBaseItem = fieldsValues[0][rowIndx] // 默认都是用第一项来处理 208 | if(columnBaseItem && columnBaseItem.text && columnBaseItem.value) { 209 | TextValues.push({ text: columnBaseItem.text, value: columnBaseItem.value }); 210 | } else if (_.isObject(columnBaseItem)){ 211 | TextValues.push({ text: safeJsonStringify(columnBaseItem), value: rowIndx }); 212 | } else { 213 | // 把每一项的值拼接起来 214 | const resValue = fieldsValues.reduce((str: string, field: any[]) => str += field[rowIndx] + " " , '') 215 | TextValues.push({ text: resValue?.trim?.(), value: resValue?.trim?.() }); 216 | } 217 | 218 | } 219 | } 220 | } 221 | } 222 | 223 | return TextValues 224 | } 225 | 226 | function safeJsonStringify(obj: any) { 227 | try { 228 | return JSON.stringify(obj); 229 | } catch (error) { 230 | return obj; 231 | } 232 | } 233 | 234 | export function replaceFormat( 235 | value: { forEach: (arg0: (v: string) => void) => void; join: (arg0: string) => void }, 236 | variable: { multi: any; includeAll: any; name: string; label: any; description: string } 237 | ) { 238 | if (typeof value === 'object' && (variable.multi || variable.includeAll)) { 239 | const a: string[] = []; 240 | value.forEach(function (v: string) { 241 | if (variable.name === variable.label || (variable.description && variable.description.indexOf('field_search') >= 0)) { 242 | a.push('"' + variable.name + '":"' + v + '"'); 243 | } else { 244 | a.push(v); 245 | } 246 | }); 247 | return a.join(' OR '); 248 | } 249 | if (_.isArray(value)) { 250 | return value.join(' OR '); 251 | } 252 | return value; 253 | } 254 | 255 | 256 | export function replaceQueryParameters(q: SLSQuery | string, options: DataQueryRequest) { 257 | if (typeof q !== 'string' && q.hide) { 258 | return; 259 | } 260 | let varQuery; 261 | if (typeof q === 'string') { 262 | varQuery = q; 263 | } else { 264 | varQuery = q.query; 265 | } 266 | let query = getTemplateSrv().replace( 267 | varQuery, 268 | options.scopedVars, 269 | replaceFormat 270 | ); 271 | 272 | const re = /\$([0-9]+)([dmhs])/g; 273 | const reArray = query.match(re); 274 | _(reArray).forEach(function (col) { 275 | const old = col; 276 | col = col.replace('$', ''); 277 | let sec = 1; 278 | if (col.indexOf('s') !== -1) { 279 | sec = 1; 280 | } else if (col.indexOf('m') !== -1) { 281 | sec = 60; 282 | } else if (col.indexOf('h') !== -1) { 283 | sec = 3600; 284 | } else if (col.indexOf('d') !== -1) { 285 | sec = 3600 * 24; 286 | } 287 | col = col.replace(/[smhd]/g, ''); 288 | let v = parseInt(col, 10); 289 | v = v * sec; 290 | console.log(old, v, col, sec, query); 291 | query = query.replace(old, String(v)); 292 | }); 293 | if (query.indexOf('#time_end') !== -1) { 294 | query = query.replace('#time_end', String(options.range.to.unix() / 1000)); 295 | } 296 | if (query.indexOf('#time_begin') !== -1) { 297 | query = query.replace('#time_begin', String(options.range.from.unix() / 1000)); 298 | } 299 | return query; 300 | } 301 | -------------------------------------------------------------------------------- /src/img/sls_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/src/img/sls_logo.jpg -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { DataSourcePlugin } from '@grafana/data'; 2 | import { SLSDataSource } from './datasource'; 3 | import { SLSConfigEditor } from './ConfigEditor'; 4 | import { SLSQueryEditor } from './QueryEditor'; 5 | import { SLSQuery, SLSDataSourceOptions } from './types'; 6 | import { SLSVariableQueryEditorWapper } from './VariableQueryEditor'; 7 | 8 | export const plugin = new DataSourcePlugin(SLSDataSource) 9 | .setConfigEditor(SLSConfigEditor) 10 | .setQueryEditor(SLSQueryEditor) 11 | .setVariableQueryEditor(SLSVariableQueryEditorWapper); 12 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json", 3 | "type": "datasource", 4 | "name": "log-service-datasource", 5 | "id": "aliyun-log-service-datasource", 6 | "metrics": true, 7 | "backend": true, 8 | "logs": true, 9 | "alerting": true, 10 | "executable": "gpx_log-service-datasource", 11 | "flow_chart_max_points": "6000000", 12 | "legacy_compatible": false, 13 | "info": { 14 | "description": "Aliyun log service datasource (backend version)", 15 | "author": { 16 | "name": "aliyun-log", 17 | "url": "https://aliyun.com/product/sls" 18 | }, 19 | "keywords": [ 20 | "sls", 21 | "aliyun", 22 | "log", 23 | "日志服务" 24 | ], 25 | "logos": { 26 | "small": "img/sls_logo.jpg", 27 | "large": "img/sls_logo.jpg" 28 | }, 29 | "links": [ 30 | { 31 | "name": "Website", 32 | "url": "https://github.com/aliyun/aliyun-log-grafana-datasource-plugin" 33 | }, 34 | { 35 | "name": "License", 36 | "url": "https://github.com/aliyun/aliyun-log-grafana-datasource-plugin/blob/master/LICENSE" 37 | } 38 | ], 39 | "screenshots": [], 40 | "version": "%VERSION%", 41 | "updated": "%TODAY%" 42 | }, 43 | "dependencies": { 44 | "grafanaDependency": ">=7.0.0", 45 | "plugins": [] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { DataQuery, DataSourceJsonData } from '@grafana/data'; 2 | 3 | export interface SLSQuery extends DataQuery { 4 | query?: string; 5 | xcol?: string; 6 | ycol?: string; 7 | logsPerPage?: number; 8 | currentPage?: number; 9 | type?: string; 10 | logstore?: string 11 | legendFormat?: string // 图例 format 12 | step?: string // promql step 13 | queryType?: 'range' | 'instant'; // 查询类型 14 | totalLogs?: number 15 | powerSql?: boolean 16 | } 17 | 18 | export const defaultQuery: Partial = { 19 | type: 'all', 20 | query: '* | select count(*) as c, __time__-__time__%60 as t group by t', 21 | xcol: 't', 22 | ycol: '', 23 | logsPerPage: 100, 24 | currentPage: 1, 25 | totalLogs: 100, 26 | powerSql: false 27 | }; 28 | export const defaultEidtorQuery: Partial = { 29 | type: 'all', 30 | query: '', 31 | xcol: '', 32 | ycol: '', 33 | logsPerPage: 100, 34 | currentPage: 1, 35 | totalLogs: 100, 36 | powerSql: false 37 | }; 38 | 39 | /** 40 | * These are options configured for each DataSource instance 41 | */ 42 | export interface SLSDataSourceOptions extends DataSourceJsonData { 43 | endpoint?: string; 44 | project?: string; 45 | logstore?: string; 46 | roleArn?: string; 47 | region?: string; 48 | headers?: HeaderWithValue[] 49 | } 50 | 51 | /** 52 | * Value that is used in the backend, but never sent over HTTP to the frontend 53 | */ 54 | export interface SLSSecureJsonData { 55 | accessKeyId?: string; 56 | accessKeySecret?: string; 57 | } 58 | 59 | // export declare type SlsLog = { 60 | // time: number; 61 | // attribute: Map; 62 | // name: string; 63 | // }; 64 | 65 | export type Header = { 66 | name: string; 67 | configured: boolean; 68 | }; 69 | 70 | export type HeaderWithValue = Header & { value: string }; 71 | 72 | export type LocalHeader = HeaderWithValue & { id: string }; 73 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@grafana/toolkit/src/config/tsconfig.plugin.json", 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "jsx": "react", 6 | "rootDir": "./src", 7 | "baseUrl": "./src", 8 | "typeRoots": ["./node_modules/@types"] 9 | } 10 | } --------------------------------------------------------------------------------