diff --git a/cli/Main.hs b/cli/Main.hs
deleted file mode 100644
--- a/cli/Main.hs
+++ /dev/null
@@ -1,61 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-import Control.Concurrent
-import Control.Concurrent.Async
-import Data.Maybe
-import qualified Data.Text as T
-import LightStep.HighLevel.IO (LightStepConfig (..), setTag, withSingletonLightStep, withSpan)
-import System.Environment
-import System.Exit
-
-seriousBusinessMain :: IO ()
-seriousBusinessMain = concurrently_ frontend backend
-  where
-    frontend =
-      withSpan "RESTful API" $ do
-        threadDelay 10000
-        setTag "foo" "bar"
-        withSpan "Kafka" $ do
-          threadDelay 20000
-          setTag "foo" "baz"
-        threadDelay 30000
-        withSpan "GraphQL" $ do
-          threadDelay 40000
-          setTag "foo" "quux"
-          withSpan "Mongodb" $ do
-            threadDelay 50000
-          setTag "lorem" "ipsum"
-          threadDelay 60000
-        withSpan "data->json" $ pure ()
-        withSpan "json->yaml" $ pure ()
-        withSpan "yaml->xml" $ pure ()
-        withSpan "xml->protobuf" $ pure ()
-        withSpan "protobuf->thrift" $ pure ()
-        withSpan "thrift->base64" $ pure ()
-        threadDelay 70000
-    backend =
-      withSpan "Background Data Science" $ do
-        threadDelay 10000
-        withSpan "Tensorflow" $ do
-          threadDelay 100000
-          setTag "learning" "deep"
-        withSpan "Torch" $ do
-          threadDelay 100000
-          setTag "learning" "very_deep"
-        withSpan "Hadoop" $ do
-          threadDelay 100000
-          setTag "learning" "super_deep"
-
-main :: IO ()
-main = do
-  token <-
-    lookupEnv "LIGHTSTEP_TOKEN" >>= \case
-      Just t -> pure $ T.pack t
-      Nothing -> do
-        putStrLn "LIGHTSTEP_TOKEN environment variable not defined"
-        exitFailure
-  host <- fromMaybe "ingest.lightstep.com" <$> lookupEnv "LIGHTSTEP_HOST"
-  port <- maybe 443 read <$> lookupEnv "LIGHTSTEP_PORT"
-  let lsConfig = LightStepConfig host port token "helloworld" 5
-  withSingletonLightStep lsConfig seriousBusinessMain
-  putStrLn "All done"
diff --git a/examples/readme/Main.hs b/examples/readme/Main.hs
new file mode 100644
--- /dev/null
+++ b/examples/readme/Main.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Concurrent
+import Control.Concurrent.Async
+import Data.Maybe
+import qualified Data.Text as T
+import LightStep.HighLevel.IO (LightStepConfig (..), LogEntryKey (..), addLog, setTag, withSingletonLightStep, withSpan)
+import System.Environment
+import System.Exit
+
+seriousBusinessMain :: IO ()
+seriousBusinessMain = concurrently_ frontend backend
+  where
+    frontend =
+      withSpan "RESTful API" $ do
+        threadDelay 10000
+        setTag "foo" "bar"
+        withSpan "Kafka" $ do
+          threadDelay 20000
+          setTag "foo" "baz"
+        threadDelay 30000
+        withSpan "GraphQL" $ do
+          threadDelay 40000
+          setTag "foo" "quux"
+          addLog Event "monkey-job"
+          addLog (Custom "foo") "bar"
+          withSpan "Mongodb" $ do
+            threadDelay 50000
+          setTag "lorem" "ipsum"
+          threadDelay 60000
+        withSpan "data->json" $ pure ()
+        withSpan "json->yaml" $ pure ()
+        withSpan "yaml->xml" $ pure ()
+        withSpan "xml->protobuf" $ pure ()
+        withSpan "protobuf->thrift" $ pure ()
+        withSpan "thrift->base64" $ pure ()
+        threadDelay 70000
+    backend =
+      withSpan "Background Data Science" $ do
+        threadDelay 10000
+        withSpan "Tensorflow" $ do
+          threadDelay 100000
+          setTag "learning" "deep"
+        withSpan "Torch" $ do
+          threadDelay 100000
+          setTag "learning" "very_deep"
+        withSpan "Hadoop" $ do
+          threadDelay 100000
+          setTag "learning" "super_deep"
+
+main :: IO ()
+main = do
+  token <-
+    lookupEnv "LIGHTSTEP_TOKEN" >>= \case
+      Just t -> pure $ T.pack t
+      Nothing -> do
+        putStrLn "LIGHTSTEP_TOKEN environment variable not defined"
+        exitFailure
+  host <- fromMaybe "ingest.lightstep.com" <$> lookupEnv "LIGHTSTEP_HOST"
+  port <- maybe 443 read <$> lookupEnv "LIGHTSTEP_PORT"
+  let lsConfig = LightStepConfig host port token "helloworld" 5
+  withSingletonLightStep lsConfig seriousBusinessMain
+  putStrLn "All done"
diff --git a/examples/req/Main.hs b/examples/req/Main.hs
new file mode 100644
--- /dev/null
+++ b/examples/req/Main.hs
@@ -0,0 +1,34 @@
+{-# language OverloadedStrings #-}
+
+import Control.Concurrent
+import Data.Maybe
+import LightStep.HighLevel.IO
+import LightStep.Propagation (headersForSpanContext)
+import Network.HTTP.Req
+import qualified Data.Text as T
+import System.Environment
+import System.Exit
+
+clientMain :: IO ()
+clientMain = do
+  withSpan "req-example" $ do
+    setTag "span.kind" "client"
+    setTag "component" "http"
+    Just ctx <- currentSpanContext
+    let opts = port 8736 <> foldMap (\(k, v) -> header k v) (headersForSpanContext ctx)
+        url = http "127.0.0.1" /: "test"
+    runReq defaultHttpConfig $ req GET url NoReqBody ignoreResponse opts
+  threadDelay 1_000_000
+
+main :: IO ()
+main = do
+  token <-
+    lookupEnv "LIGHTSTEP_TOKEN" >>= \case
+      Just t -> pure $ T.pack t
+      Nothing -> do
+        putStrLn "LIGHTSTEP_TOKEN environment variable not defined"
+        exitFailure
+  host <- fromMaybe "ingest.lightstep.com" <$> lookupEnv "LIGHTSTEP_HOST"
+  lsport <- maybe 443 read <$> lookupEnv "LIGHTSTEP_PORT"
+  let lsConfig = LightStepConfig host lsport token "example-req-client" 5
+  withSingletonLightStep lsConfig clientMain
diff --git a/examples/wai/Main.hs b/examples/wai/Main.hs
new file mode 100644
--- /dev/null
+++ b/examples/wai/Main.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Data.Maybe (fromMaybe)
+import System.Environment
+import System.Exit
+import LightStep.HighLevel.IO (LightStepConfig (..), withSingletonLightStep)
+import Network.Wai.Middleware.LightStep
+import Network.Wai
+import Network.HTTP.Types
+import Network.Wai.Handler.Warp (run)
+import qualified Data.Text as T
+
+robustScalableProductionReadyMicroservice :: Application
+robustScalableProductionReadyMicroservice = \_req respond -> do
+  putStrLn "Disrupting the industry"
+  respond $ responseLBS
+    status200
+    [("Content-Type", "text/plain")]
+    "It's all data!"
+
+main :: IO ()
+main = do
+  token <-
+    lookupEnv "LIGHTSTEP_TOKEN" >>= \case
+      Just t -> pure $ T.pack t
+      Nothing -> do
+        putStrLn "LIGHTSTEP_TOKEN environment variable not defined"
+        exitFailure
+  host <- fromMaybe "ingest.lightstep.com" <$> lookupEnv "LIGHTSTEP_HOST"
+  port <- maybe 443 read <$> lookupEnv "LIGHTSTEP_PORT"
+  let lsConfig = LightStepConfig host port token "example-wai-service" 5
+      microserviceWithTracing = tracingMiddleware robustScalableProductionReadyMicroservice
+  withSingletonLightStep lsConfig $
+    run 8736 microserviceWithTracing
diff --git a/lightstep-haskell.cabal b/lightstep-haskell.cabal
--- a/lightstep-haskell.cabal
+++ b/lightstep-haskell.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.4
 
 name:           lightstep-haskell
-version:        0.2.0
+version:        0.3.0
 synopsis:       LightStep OpenTracing client library
 description:    LightStep OpenTracing client library. Uses GRPC transport via proto-lens.
 category:       Tools
@@ -31,6 +31,7 @@
     ScopedTypeVariables
     TupleSections
     TypeApplications
+    ViewPatterns
   ghc-options:
     -Wall
     -Wcompat
@@ -50,6 +51,8 @@
       LightStep.LowLevel
       LightStep.HighLevel.IO
       LightStep.Internal.Debug
+      LightStep.Propagation
+      Network.Wai.Middleware.LightStep
   other-modules:
       Paths_lightstep_haskell
       Proto.Collector
@@ -66,19 +69,22 @@
   build-depends:
       base >=4.12 && <5
     , async
+    , bytestring
     , chronos
     , containers
     , safe-exceptions
-    , http2-client
-    , http2-client-grpc
+    , http2-client >= 0.9.0.0
+    , http2-client-grpc >= 0.7.0.0
     , lens
     , mtl
-    , proto-lens
-    , proto-lens-runtime
+    , proto-lens >= 0.5.1.0
+    , proto-lens-runtime >= 0.5.0.0
     , stm
     , text
     , transformers
     , unordered-containers
+    , http-types
+    , wai
 
 executable lightstep-haskell-example
   import: options
@@ -87,13 +93,44 @@
     -rtsopts
     -with-rtsopts=-N
   main-is: Main.hs
-  other-modules:
-      Paths_lightstep_haskell
   hs-source-dirs:
-      cli
+      examples/readme
   build-depends:
       base >=4.9 && <5
     , async
     , http2-client
     , lightstep-haskell
     , text
+
+executable lightstep-haskell-wai-example
+  import: options
+  ghc-options:
+    -threaded
+    -rtsopts
+    -with-rtsopts=-N
+  main-is: Main.hs
+  hs-source-dirs:
+      examples/wai
+  build-depends:
+      base >=4.9 && <5
+    , lightstep-haskell
+    , http-types
+    , text
+    , wai
+    , warp
+
+executable lightstep-haskell-req-example
+  import: options
+  ghc-options:
+    -threaded
+    -rtsopts
+    -with-rtsopts=-N
+  main-is: Main.hs
+  hs-source-dirs:
+      examples/req
+  build-depends:
+      base >=4.9 && <5
+    , lightstep-haskell
+    , http-types
+    , text
+    , req >= 3.0.0
diff --git a/src/LightStep/HighLevel/IO.hs b/src/LightStep/HighLevel/IO.hs
--- a/src/LightStep/HighLevel/IO.hs
+++ b/src/LightStep/HighLevel/IO.hs
@@ -28,6 +28,15 @@
 globalSharedMutableSpanStacks :: MVar (HM.HashMap ThreadId [Span])
 globalSharedMutableSpanStacks = unsafePerformIO (newMVar mempty)
 
+data LogEntryKey =  ErrorKind | Event | Message | Stack | Custom T.Text
+
+showLogEntryKey :: LogEntryKey -> T.Text
+showLogEntryKey ErrorKind  = T.pack "error.kind"
+showLogEntryKey Event      = T.pack "event"
+showLogEntryKey Message    = T.pack "message"
+showLogEntryKey Stack      = T.pack "stack"
+showLogEntryKey (Custom x) = x
+
 withSpan :: MonadIO m => MonadMask m => T.Text -> m a -> m a
 withSpan opName action =
   bracket
@@ -75,9 +84,29 @@
          in pure stacks'
     )
 
+currentSpanContext :: MonadIO m => m (Maybe SpanContext)
+currentSpanContext = liftIO $ do
+  tId <- myThreadId
+  stacks <- readMVar globalSharedMutableSpanStacks
+  let ctx =
+        case stacks HM.! tId of
+          (sp : _) -> Just $ sp ^. spanContext
+          _ -> Nothing
+  pure ctx
+    
+
 setTag :: MonadIO m => T.Text -> T.Text -> m ()
 setTag k v =
   modifyCurrentSpan (tags %~ (<> [defMessage & key .~ k & stringValue .~ v]))
+
+addLog :: LogEntryKey -> T.Text -> IO ()
+addLog k v =
+  modifyCurrentSpan (logs %~ (<> [defMessage & fields .~ [defMessage & key .~ showLogEntryKey k & stringValue .~ v]]))
+
+setParentSpanContext :: MonadIO m => SpanContext -> m ()
+setParentSpanContext ctx = modifyCurrentSpan $ \sp -> sp
+  & references .~ [defMessage & relationship .~ Reference'CHILD_OF & spanContext .~ ctx]
+  & spanContext . traceId .~ (ctx ^. traceId)
 
 {-# NOINLINE globalSharedMutableSingletonState #-}
 globalSharedMutableSingletonState :: TBQueue Span
diff --git a/src/LightStep/LowLevel.hs b/src/LightStep/LowLevel.hs
--- a/src/LightStep/LowLevel.hs
+++ b/src/LightStep/LowLevel.hs
@@ -116,6 +116,7 @@
 startSpan :: T.Text -> IO Span
 startSpan op = do
   nanosSinceEpoch <- getTime <$> now
+  -- FIXME: make those ids randomer
   let sid = fromIntegral nanosSinceEpoch
       tid = fromIntegral nanosSinceEpoch
   pure $
diff --git a/src/LightStep/Propagation.hs b/src/LightStep/Propagation.hs
new file mode 100644
--- /dev/null
+++ b/src/LightStep/Propagation.hs
@@ -0,0 +1,68 @@
+{-# language OverloadedStrings #-}
+
+module LightStep.Propagation where
+
+import Data.Bits
+import Control.Lens
+import Data.List (foldl')
+import qualified Data.ByteString as BS
+import Data.ProtoLens.Message (defMessage)
+import Network.HTTP.Types
+import Network.Wai
+import Proto.Collector
+import Proto.Collector_Fields
+import GHC.Word
+
+extractSpanContextFromRequest :: Request -> Maybe SpanContext
+extractSpanContextFromRequest = extractSpanContextFromRequestHeaders . requestHeaders
+
+extractSpanContextFromRequestHeaders :: RequestHeaders -> Maybe SpanContext
+extractSpanContextFromRequestHeaders hdrs =
+  case foldl' go (Nothing, Nothing) hdrs of
+    (Just tid, Just sid) -> defMessage
+      & traceId .~ tid
+      & spanId .~ sid
+      & Just
+    _ -> Nothing
+  where
+  -- TODO: Zipkin style propagation
+
+  go (_, sid) ("ot-tracer-traceid", decode_u64 -> Just tid) = (Just tid, sid)
+  go (tid, _) ("ot-tracer-spanid", decode_u64 -> Just sid) = (tid, Just sid)
+
+  -- TODO: Propagation of the whole span context in a single header
+  -- go _ ("x-ot-span-context", parse128 -> Just (tid, sid)) = (Just tid, Just sid)
+
+  go acc _ = acc
+
+
+  -- parse128 :: BS.ByteString -> Maybe (Word64, Word64)
+  -- parse128 _ = Nothing -- TODO
+
+headersForSpanContext :: SpanContext -> [(BS.ByteString, BS.ByteString)]
+headersForSpanContext ctx =
+  [ ("ot-tracer-traceid", encode_u64 $ ctx ^. traceId)
+  , ("ot-tracer-spanid", encode_u64 $ ctx ^. spanId)
+  ]
+
+encode_u64 :: Word64 -> BS.ByteString
+-- TODO: a better way to encode base16
+encode_u64 x =
+  let encodeChar :: Word8 -> Word8
+      encodeChar c | c < 10 = 48 + c
+      encodeChar c = 87 + c
+      hexDigits = go 16 [] x
+      go :: Int -> [Word8] -> Word64 -> [Word8]
+      go 0 acc _ = acc
+      go n acc v = go (n - 1) (encodeChar (fromIntegral v .&. 0x0f) : acc) (v `div` 16)
+  in BS.pack hexDigits
+
+decode_u64 :: BS.ByteString -> Maybe Word64
+decode_u64 bytes | BS.length bytes /= 16 = Nothing
+decode_u64 bytes = BS.foldl' go (Just 0) bytes
+  where
+  go Nothing _ = Nothing
+  go (Just !result) d | d >= 48 && d < 58 = Just $ result * 16 + fromIntegral d - 48
+  go (Just result) d | d >= 97 && d < 124 = Just $ result * 16 + fromIntegral d - 87
+  go _ _ = Nothing
+
diff --git a/src/Network/Wai/Middleware/LightStep.hs b/src/Network/Wai/Middleware/LightStep.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Wai/Middleware/LightStep.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Network.Wai.Middleware.LightStep where
+
+import qualified Data.Text as T
+import LightStep.HighLevel.IO
+import qualified Data.Text.Encoding as T
+import LightStep.Propagation
+import Network.Wai
+import Network.HTTP.Types
+
+-- Semantic conventions for HTTP spans:
+-- https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md
+
+tracingMiddleware :: Application -> Application
+tracingMiddleware app = \req sendResp -> do
+  withSpan "WAI handler" $ do
+    case extractSpanContextFromRequest req of
+      Just ctx -> do
+        setParentSpanContext ctx
+      _ -> pure ()
+    setTag "span.kind" "server"
+    setTag "component" "http"
+    setTag "http.method" $ T.decodeUtf8 (requestMethod req)
+    setTag "http.target" $ T.decodeUtf8 (rawPathInfo req)
+    setTag "http.flavor" $ showT (httpVersion req)
+    app req $ \resp -> do
+      setTag "http.status_code" $ showT (statusCode $ responseStatus resp)
+      sendResp resp
+
+showT :: Show a => a -> T.Text
+showT = T.pack . show
