/**
 * Copyright 2021 Opstrace, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ import crypto from "crypto" ; import { Response as GotResponse } from "got" ; import { ZonedDateTime , DateTimeFormatter } from "@js-joda/core" ; import { log } from "./log" ; let COUNTER_HTTP_RESP_LOG_THROTTLE = 0 ; export const httpTimeoutSettings = { connect : 10000 , request : 60000 } ; /**
 * Generate a floating point number from [min, max) with uniform distribution.
 */ export function rndFloatFromInterval ( min , max ) { return Math . random ( ) * ( max - min ) + min ; } export function logEveryNcalc ( total ) { if ( total <= 10 ) { return 1 ; } const o = Math . floor ( Math . log10 ( total ) ) ; return Math . pow ( 10 , o - 1 ) ; } /**
 * Split array into chunks, last element in return value may have less then
 * desired chunk size.
 *
 * Do not mutate original array.
 *
 * props to
 * https://stackoverflow.com/questions/8495687/split-array-into-chunks#comment114499622_24782004
 * chunkArray([1,2,3,4,5,6,7,8], 3);
 * Output: [ [1,2,3] , [4,5,6] ,[7,8] ]
 */ export function chunkify < T > ( a , chunkSize ) { const R = [ ] ; for ( let i = 0 , len = a . length ; i < len ; i += chunkSize ) R . push ( a . slice ( i , i + chunkSize ) ) ; return R ; } export function rndstring ( length = 5 ) { return crypto . randomBytes ( length + 1 ) . toString ( "base64" ) . replace ( /\//g , "_" ) . replace ( /\+/g , "_" ) . replace ( /=/g , "" ) . slice ( 0 , length ) ; } export function rndstringFast ( length = 5 ) { return crypto . randomBytes ( length + 1 ) . toString ( "base64" ) . slice ( 0 , length ) ; } export function rndstringFastBoringFill ( rndlen , boringlen ) { return ( crypto . randomBytes ( rndlen + 1 ) . toString ( "base64" ) . slice ( 0 , rndlen ) + "a" . repeat ( boringlen ) ) ; } /**
 * Translate a joda datetime object into a an integer string, indicating the
   number of nanoseconds passed since epoch. Example:

  timestamp('2001-01-05T10:00:01.123456789Z') --> '978688801123456789'

  The outcome is used common way to represent a specific point in time in the
  Prometheus ecosystem, e.g. used in the HTTP API.

  The `ZonedDateTime` object `ts` might not have a sub-second resolution, in
  which case `ts.nano()` seems to still return `0`. Zero-pad the nano-second
  part, i.e. if `ts.nano()` returns 1, append a '000000001'.
 */ export function timestampToNanoSinceEpoch ( ts ) { return `${ ts . toEpochSecond ( ) }${ ts . nano ( ) . toString ( ) . padStart ( 9 , "0" ) }` ; } export function timestampToRFC3339Nano ( ts ) { if ( ts . zone ( ) . toString ( ) !== "Z" ) throw Error ( "code assumes Zulu time" ) ; return ts . format ( DateTimeFormatter . ofPattern ( "yyyy-MM-dd'T'HH:mm:ss.n'Z'" ) ) ; } export function randomSampleFromArray ( a , n ) { const result = new Array ( n ) ; let len = a . length ; const taken = new Array ( len ) ; if ( n > len ) throw new Error ( "randomSampleFromArray(): more elements taken than available" ) ; while ( n -- ) { const x = Math . floor ( Math . random ( ) * len ) ; result [ n ] = a [ x in taken ? taken [ x ] : x ] ; taken [ x ] = -- len in taken ? taken [ len ] : len ; } return result ; } export function logHTTPResponseLight ( resp , requestDetail ? ) { let reqDetailForLog = "" ; if ( requestDetail !== undefined ) { reqDetailForLog = requestDetail ; } const t1 = ( resp . timings . phases . total ?? - 1000 ) / 1000.0 ; const t2 = ( resp . timings . phases . firstByte ?? - 1000 ) / 1000.0 ; const msg = `resp to ${ resp . request . options . method } ${ reqDetailForLog } -> ${ resp . statusCode }. total time: ${ t1 . toFixed ( 2 ) } s. resp time: ${ t2 . toFixed ( 2 ) } ` ; const logEveryN = 400 ; if ( COUNTER_HTTP_RESP_LOG_THROTTLE % 400 == 0 ) { log . info ( `${ msg } (${ logEveryN - 1 } msgs like this hidden)` ) ; } else { log . debug ( msg ) ; } COUNTER_HTTP_RESP_LOG_THROTTLE ++ ; } export function logHTTPResponse ( resp , requestDetail ? ) { let reqDetailForLog = "" ; if ( requestDetail !== undefined ) { reqDetailForLog = ` (${ requestDetail })` ; } let bodyPrefix = resp . body . slice ( 0 , 500 ) ; if ( Buffer . isBuffer ( bodyPrefix ) ) bodyPrefix = bodyPrefix . toString ( "utf-8" ) ; const ts = resp . timings ; log . info ( `HTTP resp to ${ resp . request . options . method }(${ resp . requestUrl }${ reqDetailForLog }):
    status: ${ resp . statusCode }
    body[:500]: ${ bodyPrefix }
    headers: ${ JSON . stringify ( resp . headers ) }
    totalTime: ${ ( ts . phases . total ?? - 1000 ) / 1000.0 } s
    dnsDone->TCPconnectDone: ${ ( ts . phases . tcp ?? - 1000 ) / 1000.0 } s
    connectDone->reqSent ${ ( ts . phases . request ?? - 1000 ) / 1000.0 } s
    reqSent->firstResponseByte: ${ ( ts . phases . firstByte ?? - 1000 ) / 1000.0 } s
    ` ) ; }