Logql analyzer page cherry picks (#6889)

* added Logql Analyzer page into loki docs

* changed template to support changes in API and supported the case when query is run against empty line

* added more json examples, changed some labels and changes css for line numbers

* fixed last comments from Karen and pointed to production API
pull/6890/head
Vladyslav Diachenko 3 years ago committed by GitHub
parent 3c5fdb749a
commit bec014dd86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 219
      docs/sources/logql/analyzer.md
  2. 5210
      docs/sources/logql/analyzer/handlebars.js
  3. 183
      docs/sources/logql/analyzer/script.js
  4. 222
      docs/sources/logql/analyzer/style.css

@ -0,0 +1,219 @@
---
title: LoqQL Analyzer
weight: 60
---
<link rel="stylesheet" href="../analyzer/style.css">
<script src="../analyzer/handlebars.js"></script>
# LogQL Analyzer
<main class="logql-analyzer">
<section class="logs-source panel-container">
<div class="logs-source__header">
<div class="examples">
<span>Log line format:</span>
<span class="example">
<input type="radio" class="example-select" name="example" id="logfmt-example" checked>
<label for="logfmt-example">logfmt</label>
</span>
<span class="example">
<input type="radio" class="example-select" name="example" id="json-parser-example">
<label for="json-parser-example">JSON</label>
</span>
<span class="example">
<input type="radio" class="example-select" name="example" id="pattern-parser-example">
<label for="pattern-parser-example">Unstructured text</label>
</span>
</div>
<div class="share-section">
<span class="share-link-copied-notification hide" id="share-link-copied-notification">
<i class="fa fa-check" aria-hidden="true"></i>
Link copied to clipboard.
</span>
<button class="primary-button" id="share-button">
<i class="fa fa-link" aria-hidden="true"></i>
Share
</button>
</div>
</div>
<div class="panel-header">
{job="analyze"}
</div>
<textarea id="logs-source-input" class="logs-source__input"></textarea>
</section>
<section class="query panel-container">
<div class="panel-header">
Query:
</div>
<div class="query-container">
<div class="input-box">
<span class="prefix">{job="analyze"} </span>
<input id="query-input" class="query_input">
</div>
<button class="query_submit primary-button">Run query</button>
</div>
<div class="query-error" id="query-error"></div>
</section>
<section class="results panel-container hide" id="results">
</section>
</main>
<script id="log-result-template" type="text/x-handlebars-template">
<div class="panel-header">
Results
</div>
{{#each results}}
<article class="debug-result-row">
<div class="last-stage-result" data-line-index="{{@index}}">
<div class="line-index">
<div class="line-index__wrapper">
<i class="line-cursor expand-cursor"></i>
<span>Line {{inc @index}}</span>
</div>
</div>
{{#if this.log_result}}
<span {{#if this.filtered_out}}class="filtered-out"{{/if}}>
{{this.log_result}}
</span>
{{/if}}
{{#unless this.log_result}}
<span class="note-text">(empty line)</span>
{{/unless}}
</div>
<div class="debug-result-row__explain hide">
<div class="explain-section origin-line">
<div class="explain-section__header">
Original log line
<span class="stage-expression">{{../stream_selector}}</span>
</div>
<div class="explain-section__body">
{{this.origin_line}}
{{#unless this.log_result}}
<span class="note-text">(empty line)</span>
{{/unless}}
</div>
</div>
{{#each this.stages}}
<div class="arrow-wrapper">
<i class="fa fa-arrow-down" aria-hidden="true"></i>
</div>
<div class="explain-section stage-line">
<div class="explain-section__header">
<span>stage #{{inc @index}}:</span>
<span class="stage-expression"> {{stage_expression}} </span>
</div>
<div class="explain-section__body">
<div class="explain-section__row">
<div class="explain-section__row-title">
Available labels on this stage:
</div>
<div class="explain-section__row-body">
{{#unless labels_before}}
<span>none</span>
{{/unless}}
{{#if labels_before}}
{{#each labels_before}}
<article class="label-value" style="background-color: {{background_color}}">
{{name}}={{value}}
</article>
{{/each}}
{{/if}}
</div>
</div>
<div class="explain-section__row">
<div class="explain-section__row-title">
Line after this stage:
</div>
<div class="explain-section__row-body">
{{#if line_after}}
<span {{#if this.filtered_out}}class="filtered-out"{{/if}}>
{{line_after}}
</span>
{{/if}}
{{#unless line_after}}
<span class="note-text">(empty line)</span>
{{/unless}}
{{#if this.filtered_out}}
<span class="important-text">the line has been filtered out on this stage</span>
{{/if}}
</div>
</div>
{{#if added_labels}}
<div class="explain-section__row">
<div class="explain-section__row-title">
Added/Modified labels:
</div>
<div class="explain-section__row-body">
{{#each added_labels}}
<article class="label-value" style="background-color: {{background_color}}">
{{name}}={{value}}
</article>
{{/each}}
</div>
</div>
{{/if}}
</div>
</div>
{{/each}}
</div>
</article>
{{/each}}
</script>
[//]: # (Logfmt examples)
<script type="text/plain" id="logfmt-example-logs">
level=info ts=2022-03-23T11:55:29.846163306Z caller=main.go:112 msg="Starting Grafana Enterprise Logs"
level=debug ts=2022-03-23T11:55:29.846226372Z caller=main.go:113 version=v1.3.0 branch=HEAD Revision=e071a811 LokiVersion=v2.4.2 LokiRevision=525040a3
level=warn ts=2022-03-23T11:55:45.213901602Z caller=added_modules.go:198 msg="found valid license" cluster=enterprise-logs-test-fixture
level=info ts=2022-03-23T11:55:45.214611239Z caller=server.go:269 http=[::]:3100 grpc=[::]:9095 msg="server listening on addresses"
level=debug ts=2022-03-23T11:55:45.219665469Z caller=module_service.go:64 msg=initialising module=license
level=warm ts=2022-03-23T11:55:45.219678992Z caller=module_service.go:64 msg=initialising module=server
level=error ts=2022-03-23T11:55:45.221140583Z caller=manager.go:132 msg="license manager up and running"
level=info ts=2022-03-23T11:55:45.221254326Z caller=loki.go:355 msg="Loki started"
</script>
<script type="text/plain" id="logfmt-example-query">
| logfmt | level = "info"
</script>
[//]: # (Json parser examples)
<script type="text/plain" id="json-parser-example-logs">
{"timestamp":"2022-04-26T08:53:59.61Z","level":"INFO","class":"org.springframework.boot.SpringApplication","method":"logStartupProfileInfo","file":"SpringApplication.java","line":663,"thread":"restartedMain","message":"The following profiles are active: no-schedulers,json-logging"}
{"timestamp":"2022-04-26T08:53:59.645Z","level":"DEBUG","class":"org.springframework.boot.logging.DeferredLog","method":"logTo","file":"DeferredLog.java","line":255,"thread":"restartedMain","message":"Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable"}
{"timestamp":"2022-04-26T08:53:59.645Z","level":"DEBUG","class":"org.springframework.boot.logging.DeferredLog","method":"logTo","file":"DeferredLog.java","line":255,"thread":"restartedMain","message":"For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'"}
{"timestamp":"2022-04-26T08:54:00.274Z","level":"INFO","class":"org.springframework.data.repository.config.RepositoryConfigurationDelegate","method":"registerRepositoriesIn","file":"RepositoryConfigurationDelegate.java","line":132,"thread":"restartedMain","message":"Bootstrapping Spring Data JPA repositories in DEFAULT mode."}
{"timestamp":"2022-04-26T08:54:00.327Z","level":"INFO","class":"org.springframework.data.repository.config.RepositoryConfigurationDelegate","method":"registerRepositoriesIn","file":"RepositoryConfigurationDelegate.java","line":201,"thread":"restartedMain","message":"Finished Spring Data repository scanning in 47 ms. Found 3 JPA repository interfaces."}
{"timestamp":"2022-04-26T08:54:00.704Z","level":"INFO","class":"org.springframework.boot.web.embedded.tomcat.TomcatWebServer","method":"initialize","file":"TomcatWebServer.java","line":108,"thread":"restartedMain","message":"Tomcat initialized with port(s): 8080 (http)"}
{"timestamp":"2022-06-16T10:54:47.466Z","level":"INFO","class":"org.apache.juli.logging.DirectJDKLog","method":"log","file":"DirectJDKLog.java","line":173,"thread":"restartedMain","message":"Starting service [Tomcat]"}
{"timestamp":"2022-06-16T10:54:47.467Z","level":"INFO","class":"org.apache.juli.logging.DirectJDKLog","method":"log","file":"DirectJDKLog.java","line":173,"thread":"restartedMain","message":"Starting Servlet engine: [Apache Tomcat/9.0.52]"}
</script>
<script type="text/plain" id="json-parser-example-query">
| json | level="INFO" | line_format "{{.message}}"
</script>
[//]: # (Pattern parser examples)
<script type="text/plain" id="pattern-parser-example-logs">
238.46.18.83 - - [09/Jun/2022:14:13:44 -0700] "PUT /target/next-generation HTTP/2.0" 404 19042
16.97.233.22 - - [09/Jun/2022:14:13:44 -0700] "DELETE /extensible/functionalities HTTP/1.0" 200 27913
46.201.144.32 - - [09/Jun/2022:14:13:44 -0700] "PUT /e-enable/enable HTTP/2.0" 504 26885
33.122.3.191 - corkery3759 [09/Jun/2022:14:13:44 -0700] "POST /extensible/dynamic/enable HTTP/2.0" 100 23741
94.115.144.32 - damore5842 [09/Jun/2022:14:13:44 -0700] "PUT /matrix/envisioneer HTTP/1.0" 205 29993
145.250.221.107 - price8727 [09/Jun/2022:14:13:44 -0700] "PUT /iterate/networks/e-business/action-items HTTP/1.0" 302 9718
33.201.165.66 - - [09/Jun/2022:14:13:44 -0700] "GET /web-enabled/bricks-and-clicks HTTP/1.0" 205 2353
33.83.191.176 - kling8903 [09/Jun/2022:14:13:44 -0700] "DELETE /architect HTTP/1.1" 401 13783
</script>
<script type="text/plain" id="pattern-parser-example-query">
| pattern "<_> - <_> <_> \"<method> <url> <protocol>\" <status> <_> <_> \"<_>\" <_>" | status >= 200 and status < 300
</script>
<script src="../analyzer/script.js"> </script>

File diff suppressed because one or more lines are too long

@ -0,0 +1,183 @@
let host = "https://logql-analyzer.grafana.com/2-6-x";
Handlebars.registerHelper("inc", (val) => parseInt(val) + 1);
let streamSelector = `{job="analyze"}`;
const logsSourceInputElement = document.getElementById("logs-source-input");
const queryInputElement = document.getElementById("query-input");
const resultsElement = document.getElementById("results");
function initListeners() {
[...document.getElementsByClassName("query_submit")].forEach(btn => btn.addEventListener("click", runQuery));
[...document.getElementsByClassName("example-select")].forEach(btn => {
btn.addEventListener("click", e => {
if (!btn.checked) {
return
}
loadExample(e.currentTarget.id);
runQuery();
});
});
document.getElementById("share-button").addEventListener("click", copySharableLink)
}
let linkCopiedNotificationElement = document.getElementById("share-link-copied-notification");
function copySharableLink() {
linkCopiedNotificationElement.classList.add("hide")
let extractedData = getDataFromInputs();
let urlParam = new URLSearchParams();
urlParam.set("query", extractedData.query);
extractedData.logs.forEach(line => urlParam.append("line[]", line));
let currentUrl = window.location.href;
let sharableLink = window.location.origin + window.location.pathname + "?" + urlParam.toString();
window.history.pushState(null, null, sharableLink);
navigator.clipboard.writeText(sharableLink)
.then(() => {
linkCopiedNotificationElement.classList.remove("hide");
setTimeout(() => linkCopiedNotificationElement.classList.add("hide"), 2000)
})
}
function loadCheckedExample() {
let selectedQueryExample = document.querySelector(".example-select:checked").id;
loadExample(selectedQueryExample)
}
function updateInputs(logLines, query) {
logsSourceInputElement.value = logLines.trim();
queryInputElement.value = query.trim();
}
function loadExample(exampleId) {
let logLinesElement = document.getElementById(exampleId + "-logs");
let queryExampleElement = document.getElementById(exampleId + "-query");
updateInputs(logLinesElement.innerText, queryExampleElement.innerText);
}
function toggleExplainSection(event) {
let lineSection = document.getElementsByClassName("debug-result-row").item(event.currentTarget.dataset.lineIndex);
[...lineSection.getElementsByClassName("debug-result-row__explain")].forEach(explainSection => explainSection.classList.toggle("hide"));
[...lineSection.getElementsByClassName("line-cursor")].forEach(lineCursor => {
lineCursor.classList.toggle("expand-cursor");
lineCursor.classList.toggle("collapse-cursor");
});
}
function getDataFromInputs() {
let logs = logsSourceInputElement.value.split("\n");
let query = queryInputElement.value;
return {logs, query}
}
function runQuery() {
resultsElement.classList.add("hide");
const data = getDataFromInputs();
sendRequest({...data, query: `${streamSelector} ${data.query}`})
.then((response) => handleResponse(response))
}
async function handleResponse(response) {
if (response.status !== 200) {
handleError(await response.text());
return
}
renderResponse(await response.json())
}
function handleError(error) {
document.getElementById("query-error").innerHTML = error
document.getElementById("query-error").classList.remove("hide");
resultsElement.classList.add("hide");
}
async function sendRequest(payload) {
return fetch(host + "/api/logql-analyze", {
method: 'POST', headers: {
'Accept': 'application/json', 'Content-Type': 'application/json'
}, mode: 'cors', body: JSON.stringify(payload)
});
}
function findAddedLabels(stageInfo) {
const labelsBefore = new Map(stageInfo.labels_before.map(it => [it.name, it.value]));
return stageInfo.labels_after
.filter(labelValue => labelsBefore.get(labelValue.name) !== labelValue.value);
}
function adjustStagesModel(stages, response) {
return stages.map((stageInfo, stageIndex) => {
const addedLabels = findAddedLabels(stageInfo);
return {
...stageInfo,
labels_before: computeLabelColor(stageInfo.labels_before),
labels_after: computeLabelColor(stageInfo.labels_after),
added_labels: computeLabelColor(addedLabels),
stage_expression: response.stages[stageIndex]
}
});
}
function adjustResponseModel(response) {
return {
...response,
results: response.results.map(result => {
let stages = result.stage_records;
return {
...result,
log_result: stages.length > 0 ? stages[stages.length - 1].line_after : result.origin_line,
filtered_out: stages.some(st => st.filtered_out),
stages: adjustStagesModel(stages, response)
}
})
}
}
function computeLabelColor(labels) {
return labels.map(labelValue => {
return {...labelValue, background_color: getBackgroundColor(labelValue.name)}
})
}
function initResultsSectionListeners() {
[...document.getElementsByClassName("last-stage-result")].forEach(row => row.addEventListener("click", toggleExplainSection));
}
function renderResponse(response) {
resetErrorContainer();
const adjustedResponse = adjustResponseModel(response);
const rawResultsTemplate = document.getElementById("log-result-template").innerHTML;
const template = Handlebars.compile(rawResultsTemplate);
resultsElement.innerHTML = template(adjustedResponse);
resultsElement.classList.remove("hide");
initResultsSectionListeners();
}
function resetErrorContainer() {
let errorContainer = document.getElementById("query-error");
errorContainer.classList.add("hide")
errorContainer.innerText = "";
}
function getBackgroundColor(stringInput) {
let stringUniqueHash = [...stringInput].reduce((acc, char) => {
return char.charCodeAt(0) + ((acc << 5) - acc);
}, 0);
return `hsl(${stringUniqueHash % 360}, 95%, 35%, 15%)`;
}
function loadExampleFromUrlIfExist() {
const urlSearchParams = new URLSearchParams(window.location.search);
const query = urlSearchParams.get("query");
if (!query) {
return
}
const logLines = urlSearchParams.getAll("line[]")
.join("\n");
updateInputs(logLines, query);
runQuery()
}
initListeners();
loadCheckedExample();
loadExampleFromUrlIfExist();

@ -0,0 +1,222 @@
.hide {
display: none !important;
}
.logql-analyzer {
word-break: break-all;
position: relative;
display: grid;
grid-template-rows: 4fr 1fr auto;
}
.logs-source {
display: grid;
grid-template-rows: 1fr 1fr 9fr;
}
.examples {
display: flex;
column-gap: 10px;
align-items: center;
align-content: center;
}
.example {
display: flex;
align-items: center;
column-gap: 5px;
}
.query-container {
display: grid;
grid-template-columns: 11fr 1fr;
grid-column-gap: 5px;
}
.query-error {
color: red;
}
.logs-source__input {
border: 1px solid #a0a0a0;
height: auto;
resize: vertical;
min-height: 250px;
}
.primary-button {
position: relative;
-webkit-box-align: center;
align-items: center;
padding: 0 8px;
border-radius: 2px;
line-height: 30px;
font-weight: 500;
white-space: nowrap;
background: #3871dc;
color: #fff;
border: 1px solid #0000;
}
.primary-button:hover {
background: #2c5ab0;
color: #fff;
box-shadow: rgb(24 26 27 / 20%) 0 1px 2px;
}
.panel-container {
border: 1px solid #24292e1f;
border-radius: 3px;
box-shadow: none;
margin-bottom: 10px;
padding: 10px 15px;
}
.share-link-copied-notification {
color: #1b855e;
}
.logs-source__header {
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header {
font-weight: 600;
}
.last-stage-result {
display: grid;
grid-auto-flow: column;
grid-template-columns: 1.5fr 11fr;
}
.last-stage-result:hover {
background: #fafafa;
}
.line-index {
display: flex;
column-gap: 5px;
align-items: flex-start;
}
.line-index__wrapper{
display: flex;
align-items: center;
}
.line-cursor {
display: block;
width: 16px;
height: 16px;
}
.expand-cursor {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='16' height='16' class='css-eio55b-topVerticalAlign'%3E%3Cpath d='M14.83,11.29,10.59,7.05a1,1,0,0,0-1.42,0,1,1,0,0,0,0,1.41L12.71,12,9.17,15.54a1,1,0,0,0,0,1.41,1,1,0,0,0,.71.29,1,1,0,0,0,.71-.29l4.24-4.24A1,1,0,0,0,14.83,11.29Z'%3E%3C/path%3E%3C/svg%3E");
}
.collapse-cursor {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='16' height='16' class='css-eio55b-topVerticalAlign'%3E%3Cpath d='M17,9.17a1,1,0,0,0-1.41,0L12,12.71,8.46,9.17a1,1,0,0,0-1.41,0,1,1,0,0,0,0,1.42l4.24,4.24a1,1,0,0,0,1.42,0L17,10.59A1,1,0,0,0,17,9.17Z'%3E%3C/path%3E%3C/svg%3E");
}
.debug-result-row__explain {
display: grid;
justify-items: center;
grid-row-gap: 5px;
padding: 10px 5px 10px 25px;
border-bottom: 1px solid #24292e1f;
}
.explain-section {
width: 100%;
border: 1px solid #24292e1f;
background-color: #f4f5f5;
border-radius: 2px;
}
.explain-section__header {
font-weight: 500;
border-bottom: 1px solid #24292e1f;
padding: 4px 8px;
}
.explain-section__body {
padding: 8px;
display: grid;
grid-row-gap: 10px;
}
.input-box {
display: flex;
align-items: center;
border: 1px solid #a0a0a0;
padding-left: 0.5rem;
overflow: hidden;
}
.input-box .prefix {
font-weight: 300;
color: #999;
}
.input-box input {
flex-grow: 1;
background: #fff;
border: none;
outline: none;
padding: 0.5rem;
}
.input-box:focus-within {
border-color: #777;
}
.label-value {
border: 1px solid #777;
border-radius: 10px;
padding: 3px;
}
.explain-section__row {
display: grid;
grid-template-columns: 3fr 9fr;
grid-column-gap: 10px;
}
.explain-section__row-title {
word-break: break-word;
line-height: 2rem;
}
.explain-section__row-body {
display: flex;
flex-wrap: wrap;
column-gap: 10px;
row-gap: 5px;
font-weight: 500;
align-content: center;
}
.filtered-out {
text-decoration: line-through;
font-weight: 300;
color: #999;
}
.note-text {
font-weight: 300;
color: #999;
font-style: italic;
}
.stage-expression {
color: #1f60c4;
}
.important-text {
color: #ff0000 !important;
}
Loading…
Cancel
Save