Is there a way to generate the AWS Console URLs for CloudWatch Log Group filters?
Asked Answered
H

16

35

I would like to send my users directly to a specific log group and filter but I need to be able to generate the proper URL format. For example, this URL

https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logsV2:log-groups/log-group/
%252Fmy%252Flog%252Fgroup%252Fgoes%252Fhere/log-events/$3FfilterPattern$3D$255Bincoming_ip$252C$2Buser_name$252C$2Buser_ip$2B$252C$2Btimestamp$252C$2Brequest$2B$2521$253D$2B$2522GET$2B$252Fhealth_checks$252Fall$2B*$2522$252C$2Bstatus_code$2B$253D$2B5*$2B$257C$257C$2Bstatus_code$2B$253D$2B429$252C$2Bbytes$252C$2Burl$252C$2Buser_agent$255D$26start$3D-172800000

will take you to a log group named /my/log/group/goes/here and filter messages with this pattern for the past 2 days:

[incoming_ip, user_name, user_ip , timestamp, request != "GET /health_checks/all *", status_code = 5* || status_code = 429, bytes, url, user_agent]

I can decode part of the URL but I don't know what some of the other characters should be (see below), but this doesn't really look like any standard HTML encoding to me. Does anyone know a encoder/decoder for this URL format?

%252F == /
$252C == ,
$255B == [
$255D == ]
$253D == =
$2521 == !
$2522 == "
$252F == _
$257C == |

$2B == +
$26 == &
$3D == =
$3F == ?
Hellbox answered 22/3, 2020 at 7:43 Comment(0)
H
2

I have created a bit of Ruby code that seems to satisfy the CloudWatch URL parser. I'm not sure why you have to double escape some things and then replace % with $ in others. I'm guessing there is some reason behind it but I couldn't figure out a nice way to do it, so I'm just brute forcing it. If you have something better, or know why they do this, please add a comment.

NOTE: The filter I tested with is kinda basic and I'm not sure what might need to change if you get really fancy with it.

# Basic URL that is the same across all requests
url = 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logsV2:log-groups/log-group/'

# CloudWatch log group
log_group = '/aws/my/log/group'

# Either specify the instance you want to search or leave it out to search all instances
instance = '/log-events/i-xxxxxxxxxxxx'
 OR
instance = '/log-events'

# The filter to apply.
filter = '[incoming_ip, user_name, user_ip , timestamp, request, status_code = 5*, bytes, url, user_agent]'

# Start time.  There might be an End time as well but my queries haven't used 
# that yet so I'm not sure how it's formatted.  It should be pretty similar
# though.
hours = 48
start = "&start=-#{hours*60*60*1000}"

# This will get you the final URL
final = url + CGI.escape(CGI.escape(log_group)) + instance + '$3FfilterPattern$3D' + CGI.escape(CGI.escape(filter)).gsub('%','$') + CGI.escape(start).gsub('%','$')

Hellbox answered 23/3, 2020 at 17:30 Comment(0)
K
19

First of all I'd like to thank other guys for the clues. Further goes the complete explanation how Log Insights links are constructed.

Overall it's just weirdly encoded conjunction of an object structure that works like that:

  • Part after ?queryDetail= is object representation and {} are represented by ~()

  • Object is walked down to primitive values and the latter are transformed as following:

    • encodeURIComponent(value) so that all special characters are transformed to %xx
    • replace(/%/g, "*") so that this encoding is not affected by top level ones
    • if value type is string - it is prefixed with unmatched single quote

    To illustrate:

    "Hello world" -> "Hello%20world" -> "Hello*20world" -> "'Hello*20world"
    
  • Arrays of transformed primitives are joined using ~ and as well put inside ~() construct

Then, after primitives transformation is done - object is joined using "~".

After that string is escape()d (note that not encodeURIComponent() is called as it doesn't transform ~ in JS).

After that ?queryDetail= is added.

And finally this string us encodeURIComponent()ed and as a cherry on top - % is replaced with $.

Let's see how it works in practice. Say these are our query parameters:

const expression = `fields @timestamp, @message
    | filter @message not like 'example'
    | sort @timestamp asc
    | limit 100`;

const logGroups = ["/application/sample1", "/application/sample2"];

const queryParameters = {
  end: 0,
  start: -3600,
  timeType: "RELATIVE",
  unit: "seconds",
  editorString: expression,
  isLiveTrail: false,
  source: logGroups,
};

Firstly primitives are transformed:

const expression = "'fields*20*40timestamp*2C*20*40message*0A*20*20*20*20*7C*20filter*20*40message*20not*20like*20'example'*0A*20*20*20*20*7C*20sort*20*40timestamp*20asc*0A*20*20*20*20*7C*20limit*20100";

const logGroups = ["'*2Fapplication*2Fsample1", "'*2Fapplication*2Fsample2"];

const queryParameters = {
  end: 0,
  start: -3600,
  timeType: "'RELATIVE",
  unit: "'seconds",
  editorString: expression,
  isLiveTrail: false,
  source: logGroups,
};

Then, object is joined using ~ so we have object representation string:

const objectString = "~(end~0~start~-3600~timeType~'RELATIVE~unit~'seconds~editorString~'fields*20*40timestamp*2C*20*40message*0A*20*20*20*20*7C*20filter*20*40message*20not*20like*20'example'*0A*20*20*20*20*7C*20sort*20*40timestamp*20asc*0A*20*20*20*20*7C*20limit*20100~isLiveTrail~false~source~(~'*2Fapplication*2Fsample1~'*2Fapplication*2Fsample2))"

Now we escape() it:

const escapedObject = "%7E%28end%7E0%7Estart%7E-3600%7EtimeType%7E%27RELATIVE%7Eunit%7E%27seconds%7EeditorString%7E%27fields*20*40timestamp*2C*20*40message*0A*20*20*20*20*7C*20filter*20*40message*20not*20like*20%27example%27*0A*20*20*20*20*7C*20sort*20*40timestamp*20asc*0A*20*20*20*20*7C*20limit*20100%7EisLiveTrail%7Efalse%7Esource%7E%28%7E%27*2Fapplication*2Fsample1%7E%27*2Fapplication*2Fsample2%29%29"

Now we append ?queryDetail= prefix:

const withQueryDetail = "?queryDetail=%7E%28end%7E0%7Estart%7E-3600%7EtimeType%7E%27RELATIVE%7Eunit%7E%27seconds%7EeditorString%7E%27fields*20*40timestamp*2C*20*40message*0A*20*20*20*20*7C*20filter*20*40message*20not*20like*20%27example%27*0A*20*20*20*20*7C*20sort*20*40timestamp*20asc*0A*20*20*20*20*7C*20limit*20100%7EisLiveTrail%7Efalse%7Esource%7E%28%7E%27*2Fapplication*2Fsample1%7E%27*2Fapplication*2Fsample2%29%29"

Finally we URLencode it and replace % with $ and vois la:

const result = "$3FqueryDetail$3D$257E$2528end$257E0$257Estart$257E-3600$257EtimeType$257E$2527RELATIVE$257Eunit$257E$2527seconds$257EeditorString$257E$2527fields*20*40timestamp*2C*20*40message*0A*20*20*20*20*7C*20filter*20*40message*20not*20like*20$2527example$2527*0A*20*20*20*20*7C*20sort*20*40timestamp*20asc*0A*20*20*20*20*7C*20limit*20100$257EisLiveTrail$257Efalse$257Esource$257E$2528$257E$2527*2Fapplication*2Fsample1$257E$2527*2Fapplication*2Fsample2$2529$2529"

And putting it all together:

function getInsightsUrl(queryDefinitionId, start, end, expression, sourceGroup, timeType = 'ABSOLUTE', region = 'eu-west-1') {
  const p = m => escape(m);
  const s = m => escape(m).replace(/%/gi, '*');

  const queryDetail
    = p('~(')
      + p("end~'")
      + s(end.toUTC().toISO()) // converted using Luxon
      + p("~start~'")
      + s(start.toUTC().toISO()) // converted using Luxon
      // Or use UTC instead of Local
      + p(`~timeType~'${timeType}~tz~'Local~editorString~'`)
      + s(expression)
      + p('~isLiveTail~false~queryId~\'')
      + s(queryDefinitionId)
      + p("~source~(~'") + s(sourceGroup) + p(')')
    + p(')');

  return `https://${region}.console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:logs-insights${escape(`?queryDetail=${queryDetail}`).replace(/%/gi, '$')}`;
}

Of course reverse operation can be performed as well.

That's all folks. Have fun, take care and try to avoid doing such a weird stuff yourselves. :)

Karbala answered 3/8, 2021 at 15:16 Comment(1)
Thanks for the useful answer, I extended it to also support parsing (url -> object) and generating (object -> url): refPliant
W
15

I had to do a similar thing to generate a back link to the logs for a lambda and did the following hackish thing to create the link:

const link = `https://${process.env.AWS_REGION}.console.aws.amazon.com/cloudwatch/home?region=${process.env.AWS_REGION}#logsV2:log-groups/log-group/${process.env.AWS_LAMBDA_LOG_GROUP_NAME.replace(/\//g, '$252F')}/log-events/${process.env.AWS_LAMBDA_LOG_STREAM_NAME.replace('$', '$2524').replace('[', '$255B').replace(']', '$255D').replace(/\//g, '$252F')}`
Willms answered 5/7, 2020 at 22:19 Comment(4)
This worked perfectly for me. Note if one doesn't have those environment variables at hand process.env.AWS_LAMBDA_LOG_STREAM_NAME for me was composed of ecs/main/[taskArn:resource-id].Handbook
Worked for me as well. As suggested by mxro if you don't have these variables at hand, you can use a string itself: log_group = 'ecs/main/' and replace '/' with the required stringHaggis
Apparently there is an easier link which does the encoding/replacement for you: https://console.aws.amazon.com/cloudwatch/home?region=${process.env.AWS_REGION}#logEventViewer:group=${logGroup};stream=${logStream}Lamented
@WayneB - your link works like a champ without all the fuss... thank you. I'm elevating your comment here to an answer to ensure others see this!Berners
S
11

A colleague of mine figured out that the encoding is nothing special. It is the standard URI percent encoding but applied twice (2x). In javascript you can use the encodeURIComponent function to test this out:

let inp = 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logsV2:log-groups/log-group/'

console.log(encodeURIComponent(inp))
console.log(encodeURIComponent(encodeURIComponent(inp)))

This piece of javascript produces the expected output on the second encoding stage:

https%3A%2F%2Fconsole.aws.amazon.com%2Fcloudwatch%2Fhome%3Fregion%3Dus-east-1%23logsV2%3Alog-groups%2Flog-group%2F
https%253A%252F%252Fconsole.aws.amazon.com%252Fcloudwatch%252Fhome%253Fregion%253Dus-east-1%2523logsV2%253Alog-groups%252Flog-group%252F

Caution

At least some bits use the double encoding, not the whole link though. Otherwise all special characters would occupy 4 characters after double encoding, but some still occupy only 2 characters. Hope this helps anyway ;)

Scenarist answered 22/10, 2020 at 14:48 Comment(2)
Good solution, but I think you still need to replace % with $? I have a const awsEncode = text => encodeURIComponent(encodeURIComponent(text)).replace(/%/g, '$'); function to help me do thatExcoriate
Yea makes sense, may i use your hint to edit my solution accordingly?Scenarist
E
4

My complete Javascript solution based on @isaias-b answer, which also adds a timestamp filter on the logs:

const logBaseUrl = 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logsV2:log-groups/log-group';
const encode = text => encodeURIComponent(text).replace(/%/g, '$');
const awsEncode = text => encodeURIComponent(encodeURIComponent(text)).replace(/%/g, '$');
const encodeTimestamp = timestamp => encode('?start=') + awsEncode(new Date(timestamp).toJSON());
const awsLambdaLogBaseUrl = `${logBaseUrl}/${awsEncode('/aws/lambda/')}`;
const logStreamUrl = (logGroup, logStream, timestamp) =>
  `${awsLambdaLogBaseUrl}${logGroup}/log-events/${awsEncode(logStream)}${timestamp ? encodeTimestamp(timestamp) : ''}`;
Excoriate answered 9/1, 2021 at 9:59 Comment(0)
R
3

A bit late but here is a python implementation

def get_cloud_watch_search_url(search, log_group, log_stream, region=None,):
    """Return a properly formatted url string for search cloud watch logs

    search = "{$.message: "You are amazing"}
    log_group = Is the group of message you want to search
    log_stream = The stream of logs to search
    """

    url = f'https://{region}.console.aws.amazon.com/cloudwatch/home?region={region}'

    def aws_encode(value):
        """The heart of this is that AWS likes to quote things twice with some substitution"""
        value = urllib.parse.quote_plus(value)
        value = re.sub(r"\+", " ", value)
        return re.sub(r"%", "$", urllib.parse.quote_plus(value))

    bookmark = '#logsV2:log-groups'
    bookmark += '/log-group/' + aws_encode(log_group)
    bookmark += "/log-events/" + log_stream
    bookmark += re.sub(r"%", "$", urllib.parse.quote("?filterPattern="))
    bookmark += aws_encode(search)

    return url + bookmark

This then allows you to quickly verify it.

>>> real = 'https://us-west-2.console.aws.amazon.com/cloudwatch/home?region=us-west-2#logsV2:log-groups/log-group/$252Fapp$252Fdjango/log-events/production$3FfilterPattern$3D$257B$2524.msg$253D$2522$2525s$2525s+messages+to+$2525s+pk$253D$2525d...$2522$257D'
>>> constructed = get_cloud_watch_search_url(None, search='{$.msg="%s%s messages to %s pk=%d..."}', log_group='/app/django', log_stream='production', region='us-west-2')
>>> real == constructed
True
Reims answered 10/3, 2021 at 20:5 Comment(0)
I
3

I encountered this problem recently when I wanted to generate cloudwatch insights URL. Typescript version below:

export function getInsightsUrl(
  start: Date,
  end: Date,
  query: string,
  sourceGroup: string,
  region = "us-east-1"
) {
  const p = (m: string) => escape(m);

  // encodes inner values
  const s = (m: string) => escape(m).replace(/\%/gi, "*");

  const queryDetail =
    p(`~(end~'`) +
    s(end.toISOString()) +
    p(`~start~'`) +
    s(start.toISOString()) +
    p(`~timeType~'ABSOLUTE~tz~'UTC~editorString~'`) +
    s(query) +
    p(`~isLiveTail~false~queryId~'`) +
    s(v4()) +
    p(`~source~(~'`) +
    s(sourceGroup) +
    p(`))`);

  return (
    `https://console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:logs-insights` +
    escape("?queryDetail=" + queryDetail).replace(/\%/gi, "$")
  );
}

Github GIST

Iaverne answered 27/3, 2021 at 12:6 Comment(0)
H
2

I have created a bit of Ruby code that seems to satisfy the CloudWatch URL parser. I'm not sure why you have to double escape some things and then replace % with $ in others. I'm guessing there is some reason behind it but I couldn't figure out a nice way to do it, so I'm just brute forcing it. If you have something better, or know why they do this, please add a comment.

NOTE: The filter I tested with is kinda basic and I'm not sure what might need to change if you get really fancy with it.

# Basic URL that is the same across all requests
url = 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logsV2:log-groups/log-group/'

# CloudWatch log group
log_group = '/aws/my/log/group'

# Either specify the instance you want to search or leave it out to search all instances
instance = '/log-events/i-xxxxxxxxxxxx'
 OR
instance = '/log-events'

# The filter to apply.
filter = '[incoming_ip, user_name, user_ip , timestamp, request, status_code = 5*, bytes, url, user_agent]'

# Start time.  There might be an End time as well but my queries haven't used 
# that yet so I'm not sure how it's formatted.  It should be pretty similar
# though.
hours = 48
start = "&start=-#{hours*60*60*1000}"

# This will get you the final URL
final = url + CGI.escape(CGI.escape(log_group)) + instance + '$3FfilterPattern$3D' + CGI.escape(CGI.escape(filter)).gsub('%','$') + CGI.escape(start).gsub('%','$')

Hellbox answered 23/3, 2020 at 17:30 Comment(0)
B
2

I HAVE to elevate @WayneB's answer above bc it just works. No encoding required - just follow his template. I just confirmed it works for me. Here's what he said in one of the comments above:

"Apparently there is an easier link which does the encoding/replacement for you: https://console.aws.amazon.com/cloudwatch/home?region=${process.env.AWS_REGION}#logEventViewer:group=${logGroup};stream=${logStream}"

Thanks for this answer Wayne - just wish I saw it sooner!

Berners answered 5/2, 2023 at 19:25 Comment(0)
S
1

A Python solution based on @Pål Brattberg's answer:

cloudwatch_log_template = "https://{AWS_REGION}.console.aws.amazon.com/cloudwatch/home?region={AWS_REGION}#logsV2:log-groups/log-group/{LOG_GROUP_NAME}/log-events/{LOG_STREAM_NAME}"
log_url = cloudwatch_log_template.format(
    AWS_REGION=AWS_REGION, LOG_GROUP_NAME=CLOUDWATCH_LOG_GROUP, LOG_STREAM_NAME=LOG_STREAM_NAME
)

Make sure to substitute illegal characters first (see OP) if you used any.

Sg answered 10/2, 2021 at 13:54 Comment(0)
H
1

I encountered this problem recently when I wanted to generate cloudwatch insights URL. PHP version below:

  <?php
  function getInsightsUrl($region = 'ap-northeast-1') {

    // https://mcmap.net/q/449940/-why-is-laravel-39-s-carbon-toisostring-different-from-javascript-39-s-toisostring-how-to-make-them-the-same
    $start = now()->subMinutes(2)->format('Y-m-d\TH:i:s.v\Z');
    $end = now()->addMinutes(2)->format('Y-m-d\TH:i:s.v\Z');

    $filter = 'INFO';

    $logStream = 'xxx_backend_web';
    $sourceGroup = '/ecs/xxx_backend_prod';
    // $sourceGroup = '/aws/ecs/xxx_backend~\'/ecs/xxx_backend_dev'; // multiple source group

    $query =
        "fields @timestamp, @message \n" .
        "| sort @timestamp desc\n" .
        "| filter @logStream like '$logStream'\n" .
        "| filter @message like '$filter'\n" .
        "| limit 20";

    $queryDetail = urlencode(
        ("~(end~'") .
        ($end) .
        ("~start~'") .
        ($start) .
        ("~timeType~'ABSOLUTE~tz~'Local~editorString~'") .
        ($query) .
        ("~isLiveTail~false~queryId~'") .
        ("~source~(~'") .
        ($sourceGroup) .
        ("))")
    );

    $queryDetail = preg_replace('/\%/', '$', urlencode("?queryDetail=" . $queryDetail));

    return
        "https://console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:logs-insights"
        . $queryDetail;
}
Houdon answered 5/7, 2021 at 8:24 Comment(0)
N
1

A coworker came up with the following JavaScript solution.

import JSURL from 'jsurl';

const QUERY = {
  end: 0,
  start: -3600,
  timeType: 'RELATIVE',
  unit: 'seconds',
  editorString: "fields @timestamp, @message, @logStream, @log\n| sort @timestamp desc\n| limit 200\n| stats count() by bin(30s)",
  source: ['/aws/lambda/simpleFn'],
};

function toLogsUrl(query) {
  return `#logsV2:logs-insights?queryDetail=${JSURL.stringify(query)}`;
}

toLogsUrl(QUERY);
// #logsV2:logs-insights?queryDetail=~(end~0~start~-3600~timeType~'RELATIVE~unit~'seconds~editorString~'fields*20*40timestamp*2c*20*40message*2c*20*40logStream*2c*20*40log*0a*7c*20sort*20*40timestamp*20desc*0a*7c*20limit*20200*0a*7c*20stats*20count*28*29*20by*20bin*2830s*29~source~(~'*2faws*2flambda*2fsimpleFn))
Nuggar answered 1/2, 2023 at 15:59 Comment(0)
F
1

This is jsurl https://www.npmjs.com/package/jsurl

Think of it as JSON with the following changes:

  • Curly braces ({ and }) replaced by parentheses (( and ))
  • Square brackets ([ and ]) replaced by (~ and )
  • Property names unquoted (but escaped -- see below).
  • String values prefixed by a single quote (') and escaped
  • All other JSON punctuation (colon : and comma ,) replaced by tildes (~)
  • An extra tilde (~) at the very beginning.

This is from the AWS forum https://repost.aws/questions/QUkdGEQP7rQZmDBUaB2Ai2Qg/aws-cloudwatch-log-insights-generate-url, so credit to original poster there...

The deep link of Logs Insights query is generated in three steps:

Step 1. Define the queryDetails in a JSON structure

 {
   editorString: '',
   end: '',
   queryId: '',
   source: '',
   start: '',
   timeType: '',
   tz: '',
   unit: ''
};

Step 2. It uses jsurl (https://www.npmjs.com/package/jsurl) to encode the JSON object logs-insights?queryDetail=

Step 3. It uses following map to replace the special characters inside logs-insights?queryDetail=

 {
  '!' = '$21',
  '#' = '$23',
  '%' = '$25',
  '&' = '$26',

  // Spaces are encoded as +. That's what GWT uses when we do not manually
  //   encode our routes, so it is backwards compatible with customer links
  //   generated prior to this manual encoding process.
  ' ' = '$2B',
  ':' = '$3A',
  ';' = '$3B',
  '=' = '$3D',
  '?' = '$3F',
}

Step 4. add prefix https://console.aws.amazon.com/cloudwatch/home?region=#logsV2:

To get more details, you can use some example URL and reverse the steps and see what the original JSON structure looks like.

Fradin answered 21/4, 2023 at 17:43 Comment(0)
P
1

This question isn't specific to linking in Markdown, but using a few of the previous answers I've managed to figure out a solution that includes filter patterns and time ranges.

Goal for my link:

  • Display all log streams for a log group
  • Show the last 5 minutes of logs
  • Use a JSON filter pattern, specifically {($.event_type="INFO" || $.event_type="ERROR") && $.component = "my-app"}

Details

The URL format (thanks to @WayneB for part of this) is:

https://console.aws.amazon.com/cloudwatch/home?region=<YOUR_REGION>#logEventViewer:group=<YOUR_GROUP>;stream=;start=<RELATIVE_STARTTIME>;filter=<FILTER_PATTERN>;

  • YOUR_REGION - just the region like us-east-1
  • YOUR_GROUP - log group, no encoding needed for /
  • RELATIVE_STARTTIME - I used -300000, so 5 minutes ago in milliseconds. I didn't experiment with other types of time-based filtering.
  • FILTER_PATTERN - This is where it gets fun.
    • I started with {($.event_type="INFO" || $.event_type="ERROR") && $.component = "my-app"}
    • And ended with {%28$.event_type="INFO"||$.event_type="ERROR"%29$2526$2526$.component="my-app"}
    • Much of this is specific to Markdown links. More or less, the steps in changing it involved:
      • Replace parenthesis ( and ) with %28 and %29
      • Remove spaces
      • Replace & with $2526 (or in my case, && with $2526$2526)

So I ended up with this Markdown (where the text /aws/lambda/my-lambda is linked to Cloudwatch):

[/aws/lambda/my-lambda](https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logEventViewer:group=/aws/lambda/my-lambda;stream=;start=-300000;filter={%28$.event_type="INFO"||$.event_type="ERROR"%29$2526$2526$.component="my-app"};)
Plasterboard answered 7/6, 2023 at 18:2 Comment(0)
D
1

I ended up with this solution:

const logEventToLink = (event) => {
  const start = new Date(event.timestamp).toISOString();
  return `https://console.aws.amazon.com/cloudwatch/home?region=${region}#logEventViewer:group=${logGroup};stream=${logStream};start=${start}`;
};

Based on this awesome comment by @wayne-b

Dab answered 29/5 at 12:19 Comment(0)
R
0

Since Python contributions relate to log-groups, and not to log-insights, this is my contribution. I guess that I could have done better with the inner functions though, but it is a good starting point:

from datetime import datetime, timedelta
import re
from urllib.parse import quote
 
def get_aws_cloudwatch_log_insights(query_parameters, aws_region):
    def quote_string(input_str):
        return f"""{quote(input_str, safe="~()'*").replace('%', '*')}"""


    def quote_list(input_list):
        quoted_list = ""
        for item in input_list:
            if isinstance(item, str):
                item = f"'{item}"

            quoted_list += f"~{item}"
        return f"({quoted_list})"


    params = []
    for key, value in query_parameters.items():
        if key == "editorString":
            value = "'" + quote(value)
            value = value.replace('%', '*')
        elif isinstance(value, str):
            value = "'" + value
        if isinstance(value, bool):
            value = str(value).lower()
        elif isinstance(value, list):
            value = quote_list(value)
        params += [key, str(value)]

    object_string = quote_string("~(" + "~".join(params) + ")")
    scaped_object = quote(object_string, safe="*").replace("~", "%7E")
    with_query_detail = "?queryDetail=" + scaped_object
    result = quote(with_query_detail, safe="*").replace("%", "$")

    final_url = f"https://{aws_region}.console.aws.amazon.com/cloudwatch/home?region={aws_region}#logsV2:logs-insights{result}"

    return final_url

Example:

aws_region = "eu-west-1"

query = """fields @timestamp, @message
| filter @message not like 'example'
| sort @timestamp asc
| limit 100"""

log_groups = ["/application/sample1", "/application/sample2"]

query_parameters = {
  "end": datetime.utcnow().isoformat(timespec='milliseconds') + "Z",
  "start": (datetime.utcnow() - timedelta(days=2)).isoformat(timespec='milliseconds') + "Z",
  "timeType": "ABSOLUTE",
  "unit": "seconds",
  "editorString": query,
  "isLiveTrail": False,
  "source": log_groups,
}

print(get_aws_cloudwatch_log_insights(query_parameters, aws_region))
Raymond answered 28/12, 2021 at 11:40 Comment(0)
P
0

Yet another Python solution:

from urllib.parse import quote

def aws_quote(s):
    return quote(quote(s, safe="")).replace("%", "$")

def aws_cloudwatch_url(region, log_group, log_stream):
    return "/".join([
        f"https://{region}.console.aws.amazon.com/cloudwatch/home?region={region}#logsV2:log-groups",
        "log-group",
        aws_quote(log_group),
        "log-events",
        aws_quote(log_stream),
    ])


aws_cloudwatch_url("ap-southeast-2", "/var/log/syslog", "process/pid=1")

https://ap-southeast-2.console.aws.amazon.com/cloudwatch/home?region=ap-southeast-2#logsV2:log-groups/log-group/$252Fvar$252Flog$252Fsyslog/log-events/process$252Fpid$253D1

Precocity answered 8/12, 2022 at 5:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.