packages feed

hotel-california (empty) → 0.0.1.0

raw patch · 11 files changed

+627/−0 lines, 11 filesdep +basedep +bytestringdep +hotel-californiasetup-changed

Dependencies added: base, bytestring, hotel-california, hs-opentelemetry-api, hs-opentelemetry-exporter-otlp, hs-opentelemetry-propagator-w3c, hs-opentelemetry-sdk, hs-opentelemetry-utils-exceptions, hs-opentelemetry-vendor-honeycomb, http-types, optparse-applicative, text, time, typed-process, unliftio

Files

+ CHANGELOG.md view
@@ -0,0 +1,15 @@+# Changelog for `hotel-california`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## Unreleased++## 0.0.1.0 - 2023-09-12++- Initial Release+- Introduce `exec` functionality+
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2023++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,124 @@+# hotel-california++`hotel-california` is inspired by [`Trace`](https://github.com/Pondidum/Trace/) and [`otel-cli`](https://github.com/equinix-labs/otel-cli), a pair of utilities for tracing shell scripts.+We needed something like this in order to instrument local builds, so we could understand how much time people are spending waiting on builds, tests, etc.++# Usage++The binary name is `hotel`.+Currently, the only supported command is `exec`++```sh+$ hotel exec --help+Usage: hotel exec [-s|--span-name SPAN_NAME] SCRIPT [SCRIPT...]++  Execute the given command with tracing enabled++Available options:+  -h,--help                Show this help text+  -s,--span-name SPAN_NAME The name of the span that the program reports. By+                           default, this is the script you pass in.+  SCRIPT                   The command to run, along with any arguments. Best to+                           use -- before providing the script, otherwise it may+                           pass arguments to `hotel` instead of to your script+```++Currently, the program only looks in environment variables for configuration.++- `OTEL_EXPORTER_OTLP_ENDPOINT` (with a default to `localhost:4317`)+- `OTEL_SERVICE_NAME`+- `OTEL_EXPORTER_OTLP_HEADERS`+- `OTEL_RESOURCE_ATTRIBUTES`+- and probably others, see+  [`hs-opentelemetry-sdk`](https://hackage.haskell.org/package/hs-opentelemetry-sdk)+  for more information++# Background/FAQ++## Lol what's up with the name++Well `otel-cli` is a great name for the tool.+But this is a Haskell implementation, so I need an `h` in there somewhere.+`hotel-cli` sounds good.+But wait... `cli` ... what else could I do with that?+Ahah!++Sorry++## Difference from `Trace`++The `Trace` tool has the following basic workflow:++```sh+$ TRACE_PARENT=$(trace start "build")+$ make build+$ trace finish+```++When you call `trace start "trace-name"`, it generates a `TraceId` and `SpanId` and records that to a file in the temporary directory.+The filename carries the trace ID and span ID.+The file contains the name of the trace, the start time, and any other metadata you provide.++When you call `trace finish`, the tool looks for the `TRACE_PARENT` environment variable.+It then looks in the `$TMP/traces/state` directory for a file that matches the `TRACE_PARENT`.+It loads the file, creates a `Span` with the timestamp given in the file, and then calls `span.End`.+The tool then makes a network request to report this data.++### The Problems++#### Performance++The tool allows you to start groups and run commands that will make individual spans, allowing you to understand the overall trace.+`trace group start` is similar to `trace start` - it creates a new Span ID, attaches it to the parent span, and writes that to the temporary directory.+`trace task` does something a bit more idiomatic - it creates a `Span`, runs the command you provide, and then does `Span.end`.+`trace group finish` is similar to `trace finish` - it loads the parent span information for the group, creates a `Span`, and calls `span.End` with the timestamps loaded from the group.++The toy example I did is here:++```sh+$ TRACE_PARENT=$(trace start "why"); \+  GROUP=$(trace group start "why-1"); \+  trace task "$GROUP" -- go build; \+  trace group finish "$GROUP"; \+  trace finish+```++According to Honeycomb, this spends 170ms doing `go build`, and then incurs another 650ms to complete the entire process - the `why-1` group takes roughly 370ms extra, and then the final `trace finish` call adds another 300ms.++This performance hit may not be substantial for the apparent intention of the tool - instrumenting CI builds - but it is going to be a problem for *my* intention with the tool - instrumenting local developer workflows.+300ms is significant, but not terrible if incurred once.+However, incurring that for each step we want to record?+That's a problem.++The out-of-the-box solution is to use an OpenTelemetry collector that is local to the machine, and can report the spans periodically, in the background.+This is an extra deployment step, so it'd be nice to avoid that, if possible.++#### Nesting++The API requires you to work in this manner:++```sh+$ TRACE_PARENT=$(trace start "my-trace")+$ OUTER_GROUP=$(trace group start "neat")+$ INNER_GROUP=$(trace group start "neat" "$OUTER_GROUP")+$ trace task "$INNER_GROUP" -- make build+$ trace group finish "$INNER_GROUP"+$ trace group finish "$OUTER_GROUP"+$ trace finish+```++So any time you do `trace start`, you create an identifier for a parent span.+But any time you do `trace group finish`, you look up the relevant parent span+ID and then actually *create* a *child span*.++This makes it difficult to create a span, and just "know" if you're in a root or not.+You would need this in order to provide a composable interface: shell scripts calling other shell scripts which can all record spans.++## Difference from `otel-cli`++Well, `otel-cli` solves most of the above problems.+The main entry point is `otel-cli exec`, which runs a command for you, and reports a span for it.+You can nest `otel-cli exec` calls arbitrarily, which works nicely.+However, it too had some issues, with the most challenging being a bug around signals.+I simply couldn't figure out the behavior around signals in Golang, and all available internet advice wasn't exactly helpful.+I decided then to spike out this tool.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,54 @@+module Main where++import Data.Version (showVersion)+import Paths_hotel_california (version)+import HotelCalifornia.Exec+import HotelCalifornia.Tracing (withGlobalTracing)+import Options.Applicative++data Command = Command+    { commandGlobalOptions :: GlobalOptions+    , commandSubCommand :: SubCommand+    }++data GlobalOptions = GlobalOptions++data SubCommand+    = Exec ExecArgs++optionsParser :: ParserInfo Command+optionsParser =+    info' parser' "Welcome to `hotel-california`"+  where+  -- thanks danidiaz for the blog post+    info' :: Parser a -> String -> ParserInfo a+    info' p desc = info+        (helper <*> p)+        (fullDesc <> progDesc desc)++    parser' :: Parser Command+    parser' =+        Command+            <$> generalOptionsParser+            <*> subCommandParser+            <**> simpleVersioner (showVersion version)++    generalOptionsParser =+        pure GlobalOptions++    subCommandParser :: Parser SubCommand+    subCommandParser =+        subparser $ foldMap command'+                    [ ("exec", "Execute the given command with tracing enabled", Exec <$> parseExecArgs)+                    ]++    command' (cmdName,desc,parser) =+        command cmdName (info' parser desc)++main :: IO ()+main = do+    withGlobalTracing do+        Command {..} <- execParser optionsParser+        case commandSubCommand of+            Exec execArgs ->+                runExecArgs execArgs
+ hotel-california.cabal view
@@ -0,0 +1,214 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.35.2.+--+-- see: https://github.com/sol/hpack++name:           hotel-california+version:        0.0.1.0+description:    Please see the README on GitHub at <https://github.com/parsonsmatt/hotel-california#readme>+homepage:       https://github.com/parsonsmatt/hotel-california#readme+bug-reports:    https://github.com/parsonsmatt/hotel-california/issues+author:         Matt Parsons+maintainer:     parsonsmatt@gmail.com+copyright:      2023 Matt Parsons+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md++source-repository head+  type: git+  location: https://github.com/parsonsmatt/hotel-california++library+  exposed-modules:+      HotelCalifornia+      HotelCalifornia.Exec+      HotelCalifornia.Tracing+      HotelCalifornia.Tracing.TraceParent+  other-modules:+      Paths_hotel_california+  hs-source-dirs:+      src+  default-extensions:+      BlockArguments+      DataKinds+      DefaultSignatures+      DeriveFunctor+      DeriveGeneric+      DeriveLift+      DerivingStrategies+      DerivingVia+      FlexibleContexts+      FlexibleInstances+      GADTs+      GeneralizedNewtypeDeriving+      ImportQualifiedPost+      InstanceSigs+      LambdaCase+      MultiParamTypeClasses+      MultiWayIf+      NamedFieldPuns+      NegativeLiterals+      NumericUnderscores+      OverloadedLabels+      OverloadedStrings+      PartialTypeSignatures+      PatternSynonyms+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TypeApplications+      TypeFamilies+      UndecidableInstances+      ViewPatterns+      OverloadedRecordDot+      TypeOperators+      StrictData+      ApplicativeDo+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded+  build-depends:+      base >=4.7 && <5+    , bytestring+    , hs-opentelemetry-api >=0.1.0.0+    , hs-opentelemetry-exporter-otlp+    , hs-opentelemetry-propagator-w3c+    , hs-opentelemetry-sdk >=0.0.3.6+    , hs-opentelemetry-utils-exceptions+    , hs-opentelemetry-vendor-honeycomb+    , http-types+    , optparse-applicative+    , text+    , time+    , typed-process+    , unliftio+  default-language: Haskell2010++executable hotel+  main-is: Main.hs+  other-modules:+      Paths_hotel_california+  hs-source-dirs:+      app+  default-extensions:+      BlockArguments+      DataKinds+      DefaultSignatures+      DeriveFunctor+      DeriveGeneric+      DeriveLift+      DerivingStrategies+      DerivingVia+      FlexibleContexts+      FlexibleInstances+      GADTs+      GeneralizedNewtypeDeriving+      ImportQualifiedPost+      InstanceSigs+      LambdaCase+      MultiParamTypeClasses+      MultiWayIf+      NamedFieldPuns+      NegativeLiterals+      NumericUnderscores+      OverloadedLabels+      OverloadedStrings+      PartialTypeSignatures+      PatternSynonyms+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TypeApplications+      TypeFamilies+      UndecidableInstances+      ViewPatterns+      OverloadedRecordDot+      TypeOperators+      StrictData+      ApplicativeDo+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , bytestring+    , hotel-california+    , hs-opentelemetry-api >=0.1.0.0+    , hs-opentelemetry-exporter-otlp+    , hs-opentelemetry-propagator-w3c+    , hs-opentelemetry-sdk >=0.0.3.6+    , hs-opentelemetry-utils-exceptions+    , hs-opentelemetry-vendor-honeycomb+    , http-types+    , optparse-applicative+    , text+    , time+    , typed-process+    , unliftio+  default-language: Haskell2010++test-suite hotel-california-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_hotel_california+  hs-source-dirs:+      test+  default-extensions:+      BlockArguments+      DataKinds+      DefaultSignatures+      DeriveFunctor+      DeriveGeneric+      DeriveLift+      DerivingStrategies+      DerivingVia+      FlexibleContexts+      FlexibleInstances+      GADTs+      GeneralizedNewtypeDeriving+      ImportQualifiedPost+      InstanceSigs+      LambdaCase+      MultiParamTypeClasses+      MultiWayIf+      NamedFieldPuns+      NegativeLiterals+      NumericUnderscores+      OverloadedLabels+      OverloadedStrings+      PartialTypeSignatures+      PatternSynonyms+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TypeApplications+      TypeFamilies+      UndecidableInstances+      ViewPatterns+      OverloadedRecordDot+      TypeOperators+      StrictData+      ApplicativeDo+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , bytestring+    , hotel-california+    , hs-opentelemetry-api >=0.1.0.0+    , hs-opentelemetry-exporter-otlp+    , hs-opentelemetry-propagator-w3c+    , hs-opentelemetry-sdk >=0.0.3.6+    , hs-opentelemetry-utils-exceptions+    , hs-opentelemetry-vendor-honeycomb+    , http-types+    , optparse-applicative+    , text+    , time+    , typed-process+    , unliftio+  default-language: Haskell2010
+ src/HotelCalifornia.hs view
@@ -0,0 +1,3 @@+-- | The 'HotelCalifornia' library is intended to provide an easy entry+-- point into OpenTelemetry tracing.+module HotelCalifornia where
+ src/HotelCalifornia/Exec.hs view
@@ -0,0 +1,55 @@+-- | This module defines the code for actually executing a command with tracing+-- enabled.+module HotelCalifornia.Exec where++import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NEL+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as Text+import HotelCalifornia.Tracing+import HotelCalifornia.Tracing.TraceParent+import System.Environment (getEnvironment)+import Options.Applicative+import System.Exit+import System.Process.Typed++data ExecArgs = ExecArgs+    { execArgsScript :: NonEmpty String+    , execArgsSpanName :: Maybe Text+    }++parseExecArgs :: Parser ExecArgs+parseExecArgs = do+    execArgsSpanName <- optional do+        option str $ mconcat+            [ metavar "SPAN_NAME"+            , long "span-name"+            , short 's'+            , help "The name of the span that the program reports. By default, this is the script you pass in."+            ]+    execArgsScript1 <- argument str (metavar "SCRIPT" <> help "The command to run, along with any arguments. Best to use -- before providing the script, otherwise it may pass arguments to `hotel` instead of to your script")+    execArgsScriptRest <- many $ argument str (metavar "SCRIPT...")+    pure ExecArgs+        { execArgsScript = execArgsScript1 :| execArgsScriptRest+        , ..+        }++runExecArgs :: ExecArgs -> IO ()+runExecArgs ExecArgs {..} = do+    let script =+            unwords $ NEL.toList execArgsScript+        spanName =+            fromMaybe (Text.pack script) execArgsSpanName++    inSpan' spanName \span_ -> do+        newEnv <- spanContextToEnvironment span_+        fullEnv <- mappend newEnv <$> getEnvironment++        let processConfig = shell $ unwords $ NEL.toList execArgsScript+        exitCode <- runProcess $ setEnv fullEnv $ processConfig+        case exitCode of+            ExitSuccess ->+                pure ()+            _ ->+                exitWith exitCode
+ src/HotelCalifornia/Tracing.hs view
@@ -0,0 +1,79 @@+module HotelCalifornia.Tracing+    ( module HotelCalifornia.Tracing+    , defaultSpanArguments+    ) where++import Control.Monad+import qualified Data.ByteString.Char8 as BS8+import Data.Text (Text)+import Data.Time+import HotelCalifornia.Tracing.TraceParent+import OpenTelemetry.Context as Context hiding (lookup)+import OpenTelemetry.Context.ThreadLocal (attachContext)+import OpenTelemetry.Trace hiding+       ( SpanKind(..)+       , SpanStatus(..)+       , addAttribute+       , addAttributes+       , createSpan+       , inSpan+       , inSpan'+       , inSpan''+       )+import qualified OpenTelemetry.Trace as Trace+import qualified OpenTelemetry.Vendor.Honeycomb as Honeycomb+import UnliftIO++-- | Initialize the global tracing provider for the application and run an action+--   (that action is generally the entry point of the application), cleaning+--   up the provider afterwards.+--+--   This also sets up an empty context (creating a new trace ID).+withGlobalTracing :: MonadUnliftIO m => m a -> m a+withGlobalTracing act = do+    void $ attachContext Context.empty+    liftIO setParentSpanFromEnvironment+    bracket initializeTracing shutdownTracerProvider $ \_ -> do+        -- note: this is not in a span since we don't have a root span yet so it+        -- would not wind up in the trace in a helpful way anyway+        void $+          Honeycomb.getOrInitializeHoneycombTargetInContext initializationTimeout+            `catch` \(e :: SomeException) -> do+              -- we are too early in initialization to be able to use a normal logger,+              -- but this needs to get out somehow.+              --+              -- honeycomb links are not load-bearing, so we let them just not come+              -- up if the API fails.+              liftIO . BS8.hPutStrLn stderr $ "error setting up Honeycomb trace links: " <> (BS8.pack $ displayException e)+              pure Nothing++        act+  where+    initializationTimeout = secondsToNominalDiffTime 3++initializeTracing :: MonadUnliftIO m => m TracerProvider+initializeTracing = do+  (processors, tracerOptions') <- liftIO getTracerProviderInitializationOptions+  provider <- createTracerProvider processors tracerOptions'+  setGlobalTracerProvider provider+  pure provider++globalTracer :: MonadIO m => m Tracer+globalTracer = getGlobalTracerProvider >>= \tp -> pure $ makeTracer tp "hotel-california" tracerOptions++inSpan' :: (MonadUnliftIO m) => Text -> (Span -> m a) -> m a+inSpan' spanName =+    inSpanWith' spanName defaultSpanArguments++inSpanWith :: (MonadUnliftIO m) => Text -> SpanArguments -> m a -> m a+inSpanWith spanName args action =+    inSpanWith' spanName args \_ -> action++inSpanWith' :: (MonadUnliftIO m) => Text -> SpanArguments -> (Span -> m a) -> m a+inSpanWith' spanName args action = do+    tr <- globalTracer+    Trace.inSpan'' tr spanName args action++inSpan :: (MonadUnliftIO m) => Text -> m a -> m a+inSpan spanName =+    inSpanWith spanName defaultSpanArguments
+ src/HotelCalifornia/Tracing/TraceParent.hs view
@@ -0,0 +1,49 @@+-- | This module defines the type of 'TraceParent' which can be parsed+module HotelCalifornia.Tracing.TraceParent where++import Data.Foldable (for_)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BS8+import qualified Data.Text.Encoding as TE+import qualified Data.Text as Text+import OpenTelemetry.Propagator.W3CTraceContext+import OpenTelemetry.Trace.Core (SpanContext, isRemote, wrapSpanContext, Span)+import System.Environment+import OpenTelemetry.Context.ThreadLocal+import qualified OpenTelemetry.Context as Ctxt++-- | This function looks up the @TRACEPARENT@ and @TRACECONTEXT@ environment+-- variables and returns a @'Maybe' 'SpanContext'@ constructed from them.+spanContextFromEnvironment :: IO (Maybe SpanContext)+spanContextFromEnvironment = do+    mtraceParent <- lookupEnvBS "TRACEPARENT"+    mtraceContext <- lookupEnvBS "TRACESTATE"+    pure $ decodeSpanContext mtraceParent mtraceContext+  where+    lookupEnvBS :: String -> IO (Maybe BS.ByteString)+    lookupEnvBS str = fmap (TE.encodeUtf8 . Text.pack) <$> lookupEnv str++-- | This function takes the given 'Span' and converts it into a list of+-- environment variables consisting of:+--+-- @+-- [ ( "TRACEPARENT", traceParent)+-- , ( "TRACESTATE", traceState)+-- ]+-- @+spanContextToEnvironment :: Span -> IO [(String, String)]+spanContextToEnvironment spanContext = do+    (traceParent, traceState) <- encodeSpanContext spanContext+    pure+        [ ("TRACEPARENT", BS8.unpack traceParent)+        , ("TRACESTATE", BS8.unpack traceState)+        ]+++-- | This function should be called after you've initialized and attached the+-- thread local 'Context'.+setParentSpanFromEnvironment :: IO ()+setParentSpanFromEnvironment = do+    mspanContext <- spanContextFromEnvironment+    for_ mspanContext \spanContext -> do+        adjustContext $ Ctxt.insertSpan (wrapSpanContext (spanContext {isRemote = True}))
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"