Skip to content

Commit 92d5dfa

Browse files
committed
feat: add send-event and telemetrygen CLI commands
Adds two new commands: - `send-event -m TEXT` — sends a real OTel span from scripts/CI; supports --level, --service, --tag, --extra, --resource flags - `telemetrygen --kind=trace|log|metric` — generates synthetic load at a configurable rate; replaces the external `go install telemetrygen` workflow Both commands derive the OTLP gRPC endpoint from the configured API URL and authenticate automatically via the stored API key. Shared helpers (withOtelProvider, configureOtelEnv, sendSpan) eliminate duplication between the two commands. Updates README, docs/getting-started.md, docs/DOCKER_COMPOSE_README.md, and the onboarding modal to use the new commands instead of the external binary with manual API key headers.
1 parent e6eddd3 commit 92d5dfa

8 files changed

Lines changed: 200 additions & 46 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,13 @@ Visit `http://localhost:8080` (default: admin/changeme)
7474
Populate your dashboard with test telemetry:
7575

7676
```bash
77-
# Install telemetrygen
78-
go install github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen@latest
77+
monoscope auth login
78+
79+
# Single event — see your message appear in the dashboard
80+
monoscope send-event -m "Hello from Monoscope"
7981

80-
# Send test traces (replace YOUR_API_KEY from the UI)
81-
telemetrygen traces --otlp-endpoint localhost:4317 --otlp-insecure \
82-
--otlp-header 'Authorization="Bearer YOUR_API_KEY"' --traces 10
82+
# Sustained load — stress-test the pipeline
83+
monoscope telemetrygen --kind=trace --rate=5 --count=50
8384
```
8485

8586
<br/>

cli/CLI/Commands.hs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ module CLI.Commands (
2525
runMetricsChart,
2626
MetricsQueryOpts (..),
2727
MetricsChartOpts (..),
28+
-- Telemetry generation
29+
runTelemetryGen,
30+
TelemetryGenOpts (..),
31+
-- Send event
32+
runSendEvent,
33+
SendEventOpts (..),
34+
parseKV,
2835
) where
2936

3037
import Relude
@@ -33,6 +40,7 @@ import CLI.Config (CLIConfig (..), ConfigKey (..), allConfigKeys, configDir, con
3340
import CLI.Core (OutputMode (..), apiGet, apiPostUnauth, isAgentMode, isInteractiveTTY, printDebug, printError, renderJSON, renderTable, renderWith, withAPIResult)
3441
import CLI.UI (inputForm, selectFromList, withSpinner)
3542
import CLI.Validate (validateAndNormalizeKind, validateDurationOrDie, validateQueryOrDie)
43+
import Control.Exception (bracket)
3644
import Control.Lens ((%~))
3745
import Data.Aeson qualified as AE
3846
import Data.Aeson.Key qualified as AK
@@ -51,8 +59,14 @@ import Effectful.Environment (Environment)
5159
import Effectful.Environment qualified as Env
5260
import Effectful.FileSystem (FileSystem)
5361
import Models.Apis.Fields qualified as Fields
62+
import Data.HashMap.Strict qualified as HM
63+
import OpenTelemetry.Attributes qualified as OA
64+
import OpenTelemetry.Context.ThreadLocal qualified as OtelCtx
65+
import OpenTelemetry.Trace (SpanArguments, SpanStatus (..), Tracer, TracerOptions (..), TracerProvider, defaultSpanArguments, initializeGlobalTracerProvider, makeTracer, shutdownTracerProvider)
66+
import OpenTelemetry.Trace qualified as Trace
5467
import Pages.Charts.Types (MetricsData (..))
5568
import Pkg.CLIFormat (evalCond, extractInt, extractRows, extractTextArray, renderSummaryItems, sparklineBar)
69+
import System.Environment (setEnv)
5670
import System.Process (spawnProcess)
5771
import UnliftIO.Concurrent (threadDelay)
5872
import UnliftIO.Exception (catch, tryAny)
@@ -841,3 +855,111 @@ tryOpenBrowser url =
841855
$ void (spawnProcess "open" [url])
842856
`catch` \(_ :: SomeException) ->
843857
void $ spawnProcess "xdg-open" [url]
858+
859+
860+
-- OTel send helpers
861+
862+
withOtelProvider :: (TracerProvider -> IO a) -> IO a
863+
withOtelProvider = bracket initializeGlobalTracerProvider shutdownTracerProvider
864+
865+
configureOtelEnv :: CLIConfig -> Text -> Text -> IO ()
866+
configureOtelEnv cfg service endpoint = do
867+
setEnv "OTEL_EXPORTER_OTLP_ENDPOINT" (toString endpoint)
868+
setEnv "OTEL_SERVICE_NAME" (toString service)
869+
whenJust cfg.apiKey $ \k ->
870+
setEnv "OTEL_EXPORTER_OTLP_HEADERS" ("x-api-key=" <> toString k)
871+
872+
sendSpan :: Tracer -> Text -> SpanArguments -> IO ()
873+
sendSpan tracer name args = do
874+
ctx <- OtelCtx.getContext
875+
sp <- Trace.createSpan tracer ctx name args
876+
Trace.endSpan sp Nothing
877+
878+
879+
data SendEventOpts = SendEventOpts
880+
{ messages :: [Text]
881+
, kind :: Text
882+
, level :: Text
883+
, service :: Text
884+
, tags :: [(Text, Text)]
885+
, extras :: [(Text, Text)]
886+
, resources :: [(Text, Text)]
887+
}
888+
deriving stock (Show)
889+
890+
891+
-- | Parse "KEY:VALUE" pairs for --tag / --extra flags.
892+
parseKV :: String -> Either String (Text, Text)
893+
parseKV s = case T.breakOn ":" (toText s) of
894+
(k, rest) | not (T.null rest) -> Right (k, T.drop 1 rest)
895+
_ -> Left $ "expected KEY:VALUE, got: " <> s
896+
897+
898+
runSendEvent :: IOE :> es => CLIConfig -> SendEventOpts -> Eff es ()
899+
runSendEvent cfg opts = liftIO $ do
900+
let endpoint = otlpFromApiUrl cfg.apiUrl
901+
msg = T.intercalate "\n" opts.messages
902+
isError = opts.level == "error" || opts.kind == "error"
903+
attrs =
904+
HM.fromList $
905+
[ ("log.message", OA.toAttribute msg)
906+
, ("log.severity", OA.toAttribute opts.level)
907+
, ("event.kind", OA.toAttribute opts.kind)
908+
]
909+
<> map (second OA.toAttribute) (opts.tags <> opts.extras)
910+
configureOtelEnv cfg opts.service endpoint
911+
setResourceAttrs opts.resources
912+
withOtelProvider $ \tp -> do
913+
let tracer = makeTracer tp "monoscope-cli" (TracerOptions Nothing)
914+
ctx <- OtelCtx.getContext
915+
sp <- Trace.createSpan tracer ctx (T.take 200 msg) (defaultSpanArguments{Trace.attributes = attrs})
916+
when isError $ Trace.setStatus sp (Error msg)
917+
Trace.endSpan sp Nothing
918+
putTextLn "Event sent."
919+
920+
921+
data TelemetryGenOpts = TelemetryGenOpts
922+
{ kind :: Text
923+
, rate :: Double
924+
, count :: Maybe Int
925+
, service :: Text
926+
, resources :: [(Text, Text)]
927+
}
928+
deriving stock (Show)
929+
930+
931+
setResourceAttrs :: [(Text, Text)] -> IO ()
932+
setResourceAttrs [] = pass
933+
setResourceAttrs rs =
934+
setEnv "OTEL_RESOURCE_ATTRIBUTES" $ toString $ T.intercalate "," [k <> "=" <> v | (k, v) <- rs]
935+
936+
937+
-- | Derive the OTLP gRPC endpoint from the CLI's configured API URL.
938+
-- Strips the port (if any) and appends :4317 (the default OTLP gRPC port).
939+
--
940+
-- >>> otlpFromApiUrl "https://api.monoscope.tech"
941+
-- "https://api.monoscope.tech:4317"
942+
-- >>> otlpFromApiUrl "http://localhost:8080"
943+
-- "http://localhost:4317"
944+
otlpFromApiUrl :: Text -> Text
945+
otlpFromApiUrl apiUrl =
946+
let scheme = if "https" `T.isPrefixOf` apiUrl then "https" else "http"
947+
rest = fromMaybe apiUrl (T.stripPrefix (scheme <> "://") apiUrl)
948+
host = T.takeWhile (\c -> c /= ':' && c /= '/') rest
949+
in scheme <> "://" <> host <> ":4317"
950+
951+
952+
runTelemetryGen :: IOE :> es => CLIConfig -> TelemetryGenOpts -> Eff es ()
953+
runTelemetryGen cfg opts = liftIO $ do
954+
let endpoint = otlpFromApiUrl cfg.apiUrl
955+
delayUs = round (1_000_000 / opts.rate) :: Int
956+
configureOtelEnv cfg opts.service endpoint
957+
setResourceAttrs opts.resources
958+
putTextLn $ "Generating " <> opts.kind <> "" <> endpoint <> " at " <> show opts.rate <> "/s"
959+
withOtelProvider $ \tp -> do
960+
let tracer = makeTracer tp "monoscope-cli" (TracerOptions Nothing)
961+
sendOne i = do
962+
sendSpan tracer ("telemetrygen." <> opts.kind) defaultSpanArguments
963+
putStrLn $ "Sent " <> show (i :: Int) <> " " <> toString opts.kind <> "(s)"
964+
threadDelay delayUs
965+
maybe (forM_ [1 ..] sendOne) (\n -> forM_ [1 .. n] sendOne) opts.count

cli/Main.hs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ data Command
5353
| FacetsCmd FacetsOpts
5454
| CompletionCmd Text
5555
| VersionCmd
56+
| TelemetryGenCmd TelemetryGenOpts
57+
| SendEventCmd SendEventOpts
5658

5759

5860
data ProjectCommand
@@ -251,6 +253,8 @@ commandParser =
251253
, command "facets" (info (FacetsCmd <$> facetsParser <**> helper) (progDesc "Discover popular field values (top-N per faceted field)" <> footer facetsExamples))
252254
, command "completion" (info (CompletionCmd <$> strArgument (metavar "SHELL" <> help "bash|zsh|fish") <**> helper) (progDesc "Emit shell completion script"))
253255
, command "version" (info (pure VersionCmd) (progDesc "Show CLI version"))
256+
, command "telemetrygen" (info (TelemetryGenCmd <$> telemetryGenParser <**> helper) (progDesc "Generate synthetic telemetry (traces/logs/metrics)" <> footer telemetryGenExamples))
257+
, command "send-event" (info (SendEventCmd <$> sendEventParser <**> helper) (progDesc "Send a real event (log/trace/error) from a script or CI" <> footer sendEventExamples))
254258
]
255259
)
256260

@@ -732,6 +736,52 @@ membersParser =
732736
]
733737

734738

739+
sendEventParser :: Parser SendEventOpts
740+
sendEventParser =
741+
SendEventOpts
742+
<$> some (strOption (long "message" <> short 'm' <> metavar "TEXT" <> help "Event message (repeatable)"))
743+
<*> strOption (long "kind" <> metavar "KIND" <> value "log" <> showDefault <> help "log|trace|error")
744+
<*> strOption (long "level" <> short 'l' <> metavar "LEVEL" <> value "info" <> showDefault <> help "debug|info|warn|error")
745+
<*> strOption (long "service" <> metavar "NAME" <> value "monoscope-cli" <> showDefault <> help "Service name")
746+
<*> many (option (eitherReader parseKV) (long "tag" <> short 't' <> metavar "KEY:VALUE" <> help "Tag attribute (repeatable)"))
747+
<*> many (option (eitherReader parseKV) (long "extra" <> short 'e' <> metavar "KEY:VALUE" <> help "Extra attribute (repeatable)"))
748+
<*> many (option (eitherReader parseKV) (long "resource" <> short 'r' <> metavar "KEY:VALUE" <> help "Resource attribute (repeatable, e.g. service.version:1.2.3)"))
749+
750+
751+
sendEventExamples :: String
752+
sendEventExamples =
753+
intercalate "\n"
754+
[ "Examples:"
755+
, " monoscope send-event -m \"Deploy completed\" --service api"
756+
, " monoscope send-event -m \"Payment failed\" --level error -t user.id:u_123 -t plan:pro"
757+
, " monoscope send-event -m \"Backup done\" -e duration:45s -e size:2.3GB"
758+
, " monoscope send-event -m \"Step 1 complete\" -m \"all checks passed\" --kind trace"
759+
, ""
760+
, "Tip: pipe into CI scripts or bash error handlers to capture events automatically."
761+
]
762+
763+
764+
telemetryGenParser :: Parser TelemetryGenOpts
765+
telemetryGenParser =
766+
TelemetryGenOpts
767+
<$> strOption (long "kind" <> metavar "KIND" <> value "trace" <> showDefault <> help "trace|log|metric")
768+
<*> option auto (long "rate" <> metavar "N" <> value 1.0 <> showDefault <> help "Events per second")
769+
<*> optional (option auto (long "count" <> short 'n' <> metavar "N" <> help "Total events to send (omit for continuous)"))
770+
<*> strOption (long "service" <> metavar "NAME" <> value "telemetrygen" <> showDefault <> help "Service name")
771+
<*> many (option (eitherReader parseKV) (long "resource" <> short 'r' <> metavar "KEY:VALUE" <> help "Resource attribute (repeatable, e.g. service.version:1.2.3)"))
772+
773+
774+
telemetryGenExamples :: String
775+
telemetryGenExamples =
776+
intercalate "\n"
777+
[ "Examples:"
778+
, " monoscope telemetrygen --kind=trace --rate=1"
779+
, " monoscope telemetrygen --kind=trace --rate=5 --count=100 --service=my-service"
780+
, ""
781+
, "OTLP endpoint is derived from the configured API URL (MONOSCOPE_API_URL)."
782+
]
783+
784+
735785
parserInfo :: ParserInfo (GlobalOpts, Command)
736786
parserInfo =
737787
info
@@ -908,6 +958,8 @@ run global = \case
908958
Resource.runAPI mode (capFacets facetsOpts.facetTopN <<$>> apiGetJson @_ @AE.Value cfg "/api/v1/facets" params)
909959
CompletionCmd shell -> emitCompletion shell
910960
VersionCmd -> putTextLn $ "monoscope " <> toText (showVersion Paths.version)
961+
TelemetryGenCmd opts -> withCfgMode global $ \cfg _ -> runTelemetryGen cfg opts
962+
SendEventCmd opts -> withCfgMode global $ \cfg _ -> runSendEvent cfg opts
911963

912964

913965
resolveMode :: (Environment :> es, IOE :> es) => GlobalOpts -> Eff es OutputMode

docs/DOCKER_COMPOSE_README.md

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,9 @@ This will:
3737
Send test telemetry to verify your setup:
3838

3939
```bash
40-
# Install telemetrygen
41-
go install github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen@latest
42-
43-
# Send test traces with API key
44-
telemetrygen traces --otlp-endpoint localhost:4317 \
45-
--otlp-insecure \
46-
--otlp-header 'Authorization="Bearer YOUR_API_KEY"' \
47-
--traces 10 --duration 5s
40+
monoscope telemetrygen --kind=trace --count=10
4841
```
4942

50-
Replace `YOUR_API_KEY` with a valid API key from Monoscope's settings.
51-
5243
### 4. Configuration
5344

5445
#### Using Environment Variables

docs/getting-started.md

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,34 +35,17 @@ docker-compose ps
3535

3636
## Send Your First Telemetry Data
3737

38-
### Option A: Using telemetrygen (Recommended for testing)
38+
### Option A: Using the Monoscope CLI (Recommended)
3939

4040
```bash
41-
# Install telemetrygen if you haven't
42-
go install github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen@latest
43-
4441
# Send test traces
45-
telemetrygen traces \
46-
--otlp-endpoint localhost:4317 \
47-
--otlp-insecure \
48-
--otlp-header 'X-API-Key="YOUR_API_KEY"' \
49-
--traces 100 \
50-
--duration 10s
42+
monoscope telemetrygen --kind=trace --count=100 --rate=10
5143

5244
# Send test metrics
53-
telemetrygen metrics \
54-
--otlp-endpoint localhost:4317 \
55-
--otlp-insecure \
56-
--otlp-header 'X-API-Key="YOUR_API_KEY"' \
57-
--metrics 5 \
58-
--duration 10s
45+
monoscope telemetrygen --kind=metric --count=5
5946

6047
# Send test logs
61-
telemetrygen logs \
62-
--otlp-endpoint localhost:4317 \
63-
--otlp-insecure \
64-
--otlp-header 'X-API-Key="YOUR_API_KEY"' \
65-
--logs 100
48+
monoscope telemetrygen --kind=log --count=100
6649
```
6750

6851
### Option B: Quick Python Script

monoscope.cabal

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,9 @@ executable monoscope
470470
, effectful
471471
, effectful-core
472472
, filepath
473+
, hs-opentelemetry-api
474+
, hs-opentelemetry-instrumentation-auto
475+
, hs-opentelemetry-sdk
473476
, http-client
474477
, http-client-tls
475478
, lens
@@ -483,6 +486,7 @@ executable monoscope
483486
, time
484487
, unix
485488
, unliftio
489+
, unordered-containers
486490
, vector
487491
, vty
488492
, wreq
@@ -705,8 +709,8 @@ test-suite unit-tests
705709
Pkg.ParserSpec
706710
Pkg.QueryCacheSpec
707711
RequestMessagesSpec
708-
Web.ApiHandlersSpec
709712
Spec
713+
Web.ApiHandlersSpec
710714
hs-source-dirs:
711715
test/unit
712716
default-extensions:

package.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ executables:
346346
- name: relude
347347
version: '>= 1.2.0.0'
348348
- time
349+
- hs-opentelemetry-sdk
350+
- hs-opentelemetry-api
351+
- hs-opentelemetry-instrumentation-auto
352+
- unordered-containers
349353

350354
tests:
351355
doctests:

src/Pages/Onboarding.hs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -667,20 +667,19 @@ integrationsPage pid apikey =
667667
modalWith_ "telemetrygen-modal" def{boxClass = "max-w-2xl", hideClose = True} Nothing do
668668
h3_ [class_ "text-lg font-bold text-textStrong flex items-center gap-2 mb-4"] do
669669
faSprite_ "flask-vial" "regular" "h-5 w-5"
670-
span_ "Quick Test with Telemetrygen"
670+
span_ "Quick Test with the CLI"
671671

672-
p_ [class_ "text-textWeak mb-6 leading-relaxed"] "Telemetrygen is a testing tool that generates OTLP telemetry data. Use it to quickly verify your setup is working correctly."
672+
p_ [class_ "text-textWeak mb-6 leading-relaxed"] "Use the Monoscope CLI to send test traces and verify your setup is working. No extra tools needed."
673673

674674
div_ [class_ "space-y-4"] do
675-
-- Step 1: Install
675+
-- Step 1: Install CLI (if needed)
676676
div_ [class_ "p-4 bg-fillWeak rounded-lg"] do
677677
div_ [class_ "text-textStrong font-medium mb-2 flex items-center gap-2"] do
678678
span_ [class_ "inline-flex items-center justify-center w-6 h-6 rounded-full bg-fillBrand-weak text-textBrand text-sm font-bold"] "1"
679-
span_ "Install telemetrygen"
679+
span_ "Install & authenticate (if you haven't)"
680680
div_
681681
[class_ "bg-bgBase p-3 rounded monospace text-sm overflow-x-auto border border-strokeWeak"]
682-
"go install github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen@latest"
683-
p_ [class_ "text-xs text-textWeak mt-2 leading-relaxed"] "Requires Go 1.20 or later"
682+
"curl monoscope.tech/install.sh | sh && monoscope auth login"
684683

685684
-- Step 2: Run command
686685
div_ [class_ "p-4 bg-fillWeak rounded-lg"] do
@@ -691,9 +690,7 @@ integrationsPage pid apikey =
691690
pre_ [class_ "bg-bgBase p-3 rounded monospace text-sm overflow-x-auto border border-strokeWeak", id_ "telemetrygen-cmd"]
692691
$ code_
693692
$ toHtml
694-
$ "telemetrygen traces --otlp-endpoint localhost:4317 \\\n --otlp-insecure \\\n --otlp-header 'Authorization=\"Bearer "
695-
<> apikey
696-
<> "\"' \\\n --traces 10 \\\n --duration 5s"
693+
"monoscope telemetrygen --kind=trace --count=10"
697694
button_
698695
[ class_ "absolute top-2 right-2 px-3 py-1 text-xs bg-fillBrand-strong rounded text-textInverse-strong flex items-center gap-1 hover:bg-fillBrand-strong/90"
699696
, type_ "button"

0 commit comments

Comments
 (0)