packages feed

hs-opentelemetry-instrumentation-postgresql-simple 0.2.0.1 → 1.0.0.0

raw patch · 6 files changed

+148/−44 lines, 6 filesdep +hs-opentelemetry-instrumentation-postgresql-simpledep +hs-opentelemetry-semantic-conventionsdep +hspecdep ~hs-opentelemetry-api

Dependencies added: hs-opentelemetry-instrumentation-postgresql-simple, hs-opentelemetry-semantic-conventions, hspec

Dependency ranges changed: hs-opentelemetry-api

Files

ChangeLog.md view
@@ -2,6 +2,17 @@  ## Unreleased +## 1.0.0.0 - 2026-05-29++- **Fix: Use `databaseOption` instead of `httpOption` for database convention selection.**+  Database attribute naming is now controlled independently via `OTEL_SEMCONV_STABILITY_OPT_IN=database`+  (or `database/dup`) rather than piggybacking on the HTTP stability setting.+- **Update stable database attributes.** `db.system` → `db.system.name`,+  `db.name` → `db.namespace`, `db.user` removed (security, per spec). `db.statement`+  uses `db.query.text` in stable mode.+- Add `db.operation.name` from first SQL keyword (e.g. `SELECT`, `INSERT`).+- Use `{operation} {db}` span name pattern (e.g. `SELECT mydb`).+ ## 0.2.0.1  - Fix error thrown if no rows passed in
LICENSE view
@@ -1,4 +1,4 @@-Copyright Ian Duncan (c) 2021+Copyright Ian Duncan (c) 2021-2026  All rights reserved. 
README.md view
@@ -1,1 +1,3 @@ # hs-opentelemetry-instrumentation-postgresql-simple++[![hs-opentelemetry-instrumentation-postgresql-simple](https://img.shields.io/hackage/v/hs-opentelemetry-instrumentation-postgresql-simple?style=flat-square&logo=haskell&label=hs-opentelemetry-instrumentation-postgresql-simple&labelColor=5D4F85)](https://hackage.haskell.org/package/hs-opentelemetry-instrumentation-postgresql-simple)
hs-opentelemetry-instrumentation-postgresql-simple.cabal view
@@ -1,20 +1,22 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.37.0.+-- This file has been generated from package.yaml by hpack version 0.38.3. -- -- see: https://github.com/sol/hpack -name:               hs-opentelemetry-instrumentation-postgresql-simple-version:            0.2.0.1-description:        Please see the README on GitHub at <https://github.com/iand675/hs-opentelemetry/tree/main/instrumentation/postgresql-simple#readme>-homepage:           https://github.com/iand675/hs-opentelemetry#readme-bug-reports:        https://github.com/iand675/hs-opentelemetry/issues-author:             Ian Duncan, Jade Lovelace-maintainer:         ian@iankduncan.com-copyright:          2024 Ian Duncan, Mercury Technologies-license:            BSD3-license-file:       LICENSE-build-type:         Simple+name:           hs-opentelemetry-instrumentation-postgresql-simple+version:        1.0.0.0+synopsis:       OpenTelemetry instrumentation for postgresql-simple+description:    Please see the README on GitHub at <https://github.com/iand675/hs-opentelemetry/tree/main/instrumentation/postgresql-simple#readme>+category:       OpenTelemetry, Database, PostgreSQL+homepage:       https://github.com/iand675/hs-opentelemetry#readme+bug-reports:    https://github.com/iand675/hs-opentelemetry/issues+author:         Ian Duncan, Jade Lovelace+maintainer:     ian@iankduncan.com+copyright:      2024 Ian Duncan, Mercury Technologies+license:        BSD3+license-file:   LICENSE+build-type:     Simple extra-source-files:     README.md     ChangeLog.md@@ -33,7 +35,8 @@   build-depends:       base >=4.7 && <5     , bytestring-    , hs-opentelemetry-api ==0.3.*+    , hs-opentelemetry-api ==1.0.*+    , hs-opentelemetry-semantic-conventions >=1.40 && <2     , iproute     , postgresql-libpq     , postgresql-simple@@ -41,4 +44,19 @@     , unliftio     , unliftio-core     , unordered-containers+  default-language: Haskell2010++test-suite hs-opentelemetry-instrumentation-postgresql-simple-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_hs_opentelemetry_instrumentation_postgresql_simple+  hs-source-dirs:+      test+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , bytestring+    , hs-opentelemetry-instrumentation-postgresql-simple ==1.0.*+    , hspec   default-language: Haskell2010
src/OpenTelemetry/Instrumentation/PostgresqlSimple.hs view
@@ -3,9 +3,9 @@ {-# LANGUAGE TemplateHaskell #-}  {- |-[New HTTP semantic conventions have been declared stable.](https://opentelemetry.io/blog/2023/http-conventions-declared-stable/#migration-plan) Opt-in by setting the environment variable OTEL_SEMCONV_STABILITY_OPT_IN to-- "http" - to use the stable conventions-- "http/dup" - to emit both the old and the stable conventions+[Database semantic conventions have been declared stable.](https://opentelemetry.io/docs/specs/semconv/non-normative/db-migration/) Opt-in by setting the environment variable OTEL_SEMCONV_STABILITY_OPT_IN to+- "database" - to use the stable conventions+- "database/dup" - to emit both the old and the stable conventions Otherwise, the old conventions will be used. The stable conventions will replace the old conventions in the next major release of this library. -} module OpenTelemetry.Instrumentation.PostgresqlSimple (@@ -47,6 +47,9 @@    -- * Utility functions   pgsSpan,++  -- * Span naming helpers (exported for testing)+  extractOperationName, ) where  import Control.Monad.IO.Class@@ -57,7 +60,6 @@ import Data.Int (Int64) import Data.List import Data.Maybe (catMaybes)-import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Database.PostgreSQL.LibPQ as LibPQ@@ -91,10 +93,11 @@   withConnection,  ) import GHC.Stack+import OpenTelemetry.Attributes.Key (unkey) import OpenTelemetry.Resource ((.=), (.=?))+import qualified OpenTelemetry.SemanticConventions as SC import OpenTelemetry.SemanticsConfig import OpenTelemetry.Trace.Core as TC-import OpenTelemetry.Trace.Monad import Text.Read (readMaybe) import UnliftIO @@ -110,55 +113,71 @@       <*> LibPQ.port pqConn    let stableMaybeAttributes =-        [ "db.system" .= toAttribute ("postgresql" :: T.Text)-        , "db.user" .=? (TE.decodeUtf8 <$> mUser)-        , "db.name" .=? (TE.decodeUtf8 <$> mDb)-        , "server.port"-            .=? ( do-                    port <- TE.decodeUtf8 <$> mPort-                    (readMaybe $ T.unpack port) :: Maybe Int-                )+        [ unkey SC.db_system_name .= toAttribute ("postgresql" :: T.Text)+        , unkey SC.db_namespace .=? (TE.decodeUtf8 <$> mDb)+        , unkey SC.server_port+            .=? (mPort >>= fmap fst . C.readInt)         , case (readMaybe . C.unpack) =<< mHost of-            Nothing -> "server.address" .=? (TE.decodeUtf8 <$> mHost)-            Just (IPv4 ipv4) -> "server.address" .= T.pack (show ipv4)-            Just (IPv6 ipv6) -> "server.address" .= T.pack (show ipv6)+            Nothing -> unkey SC.server_address .=? (TE.decodeUtf8 <$> mHost)+            Just (IPv4 ipv4) -> unkey SC.server_address .= T.pack (show ipv4)+            Just (IPv6 ipv6) -> unkey SC.server_address .= T.pack (show ipv6)         ]       oldMaybeAttributes =-        [ "db.system" .= toAttribute ("postgresql" :: T.Text)-        , "db.user" .=? (TE.decodeUtf8 <$> mUser)-        , "db.name" .=? (TE.decodeUtf8 <$> mDb)-        , "net.peer.port"-            .=? ( do-                    port <- TE.decodeUtf8 <$> mPort-                    (readMaybe $ T.unpack port) :: Maybe Int-                )+        [ unkey SC.db_system .= toAttribute ("postgresql" :: T.Text)+        , unkey SC.db_user .=? (TE.decodeUtf8 <$> mUser)+        , unkey SC.db_name .=? (TE.decodeUtf8 <$> mDb)+        , unkey SC.net_peer_port+            .=? (mPort >>= fmap fst . C.readInt)         , case (readMaybe . C.unpack) =<< mHost of-            Nothing -> "net.peer.name" .=? (TE.decodeUtf8 <$> mHost)-            Just (IPv4 ipv4) -> "net.peer.ip" .= T.pack (show ipv4)-            Just (IPv6 ipv6) -> "net.peer.ip" .= T.pack (show ipv6)+            Nothing -> unkey SC.net_peer_name .=? (TE.decodeUtf8 <$> mHost)+            Just (IPv4 ipv4) -> unkey SC.net_peer_ip .= T.pack (show ipv4)+            Just (IPv6 ipv6) -> unkey SC.net_peer_ip .= T.pack (show ipv6)         ]    semanticsOptions <- getSemanticsOptions   pure $     H.fromList $       catMaybes $-        case httpOption semanticsOptions of+        case databaseOption semanticsOptions of           Stable -> stableMaybeAttributes           StableAndOld -> stableMaybeAttributes `union` oldMaybeAttributes           Old -> oldMaybeAttributes  +extractOperationName :: C.ByteString -> Maybe T.Text+extractOperationName stmt =+  let trimmed = C.dropWhile (\c -> c == ' ' || c == '\n' || c == '\r' || c == '\t') stmt+      keyword = C.takeWhile (\c -> c /= ' ' && c /= '\n' && c /= '\r' && c /= '\t' && c /= '(') trimmed+  in if C.null keyword+       then Nothing+       else Just $ T.toUpper $ TE.decodeUtf8 keyword++ -- | Function to help with wrapping functions in postgresql-simple pgsSpan :: HasCallStack => Connection -> C.ByteString -> IO a -> IO a pgsSpan conn statement f = do   connAttr <- staticConnectionAttributes conn   dbName <- maybe "unknown db" TE.decodeUtf8 <$> withConnection conn LibPQ.db-  let callAttr = H.fromList [("db.statement", toAttribute $ TE.decodeUtf8 statement)]+  opts <- getSemanticsOptions+  let stmtText = TE.decodeUtf8 statement+      mOpName = extractOperationName statement+      stableAttrs =+        H.fromList $+          (unkey SC.db_query_text, toAttribute stmtText)+            : maybe [] (\op -> [(unkey SC.db_operation_name, toAttribute op)]) mOpName+      oldAttrs = H.fromList [(unkey SC.db_statement, toAttribute stmtText)]+      callAttr = case databaseOption opts of+        Stable -> stableAttrs+        StableAndOld -> stableAttrs <> oldAttrs+        Old -> oldAttrs       attrs = connAttr <> callAttr+      spanName = case mOpName of+        Just op -> op <> " " <> dbName+        Nothing -> dbName       spanArgs = SpanArguments Client attrs [] Nothing   tracerProvider <- getGlobalTracerProvider   let tracer = makeTracer tracerProvider $detectInstrumentationLibrary tracerOptions-  TC.inSpan tracer dbName spanArgs f+  TC.inSpan tracer spanName spanArgs f   -- | Instrumented version of 'Simple.query'
+ test/Spec.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import qualified Data.ByteString.Char8 as C+import OpenTelemetry.Instrumentation.PostgresqlSimple (extractOperationName)+import Test.Hspec+++main :: IO ()+main = hspec spec+++spec :: Spec+spec = do+  describe "extractOperationName" $ do+    it "extracts SELECT" $+      extractOperationName "SELECT * FROM users WHERE id = ?" `shouldBe` Just "SELECT"++    it "extracts INSERT" $+      extractOperationName "INSERT INTO users (name, email) VALUES (?, ?)" `shouldBe` Just "INSERT"++    it "extracts UPDATE" $+      extractOperationName "UPDATE users SET active = true WHERE id = ?" `shouldBe` Just "UPDATE"++    it "extracts DELETE" $+      extractOperationName "DELETE FROM users WHERE expired = true" `shouldBe` Just "DELETE"++    it "handles leading whitespace and newlines" $+      extractOperationName "  \n\t  SELECT 1" `shouldBe` Just "SELECT"++    it "uppercases mixed-case keywords" $+      extractOperationName "select * from t" `shouldBe` Just "SELECT"++    it "returns Nothing for bare parenthesized subquery" $+      extractOperationName "(SELECT 1)" `shouldBe` Nothing++    it "returns Nothing for empty input" $+      extractOperationName C.empty `shouldBe` Nothing++    it "returns Nothing for whitespace only" $+      extractOperationName "   \t\n  " `shouldBe` Nothing++    it "extracts CREATE" $+      extractOperationName "CREATE TABLE IF NOT EXISTS foo (id INT)" `shouldBe` Just "CREATE"++    it "extracts ALTER" $+      extractOperationName "ALTER TABLE users ADD COLUMN age INT" `shouldBe` Just "ALTER"++    it "extracts DROP" $+      extractOperationName "DROP TABLE IF EXISTS temp_data" `shouldBe` Just "DROP"++    it "extracts EXPLAIN" $+      extractOperationName "EXPLAIN ANALYZE SELECT * FROM users" `shouldBe` Just "EXPLAIN"