Mehedi Hassan Piash | Senior Software Engineer | Android | iOS | KMP | Ktor | Jetpack Compose | React-Native.

October 11, 2024

Enhanced Logging in ktor server with CallLogging Feature

October 11, 2024 Posted by Piash , , , No comments

When building web applications, especially APIs, it’s crucial to have robust logging to monitor requests, performance, and errors. Ktor provides a convenient CallLogging feature that can be easily integrated into your server setup to log important information about each HTTP request and response. This logging can be fine-tuned to include custom formatting, highlighting, and filtering.

In this article, we will explore how to use Ktor’s CallLogging to log detailed information for each incoming request, including HTTP method, request path, query parameters, response status, and more. We will also demonstrate how to color-code the logs for better readability in the console.

Call logger

Setting Up Call Logging in Ktor

To get started, you need to install the CallLogging feature in your Ktor server. Here is a code snippet that demonstrates a highly customized logging configuration:

install(CallLogging){
level = Level.INFO
filter { call -> call.request.path().startsWith("/") }
format { call ->
val status = call.response.status()
val httpMethod = call.request.httpMethod.value
val userAgent = call.request.headers["User-Agent"]
val path = call.request.path()
val queryParams =
call.request.queryParameters
.entries()
.joinToString(", ") { "${it.key}=${it.value}" }
val duration = call.processingTimeMillis()
val remoteHost = call.request.origin.remoteHost
val coloredStatus =
when {
status == null -> "\u001B[33mUNKNOWN\u001B[0m"
status.value < 300 -> "\u001B[32m$status\u001B[0m"
status.value < 400 -> "\u001B[33m$status\u001B[0m"
else -> "\u001B[31m$status\u001B[0m"
}
val coloredMethod = "\u001B[36m$httpMethod\u001B[0m"
"""
|
|------------------------ Request Details ------------------------
|Status: $coloredStatus
|Method: $coloredMethod
|Path: $path
|Query Params: $queryParams
|Remote Host: $remoteHost
|User Agent: $userAgent
|Duration: ${duration}ms
|------------------------------------------------------------------
|
"""
.trimMargin()
}
}

Key Features of This Configuration

1. Log Level:

The level = Level.INFO specifies that the logging will capture INFO level messages. This can be adjusted based on your needs (e.g., DEBUG, WARN, ERROR).

2. Filtering:

The filter function ensures that only requests starting from the root path (”/”) are logged. You can customize this to log requests to specific paths or APIs.

3. Custom Format:

The format block allows you to create a personalized log message that includes key details about each request:

• Status: The HTTP response status code (e.g., 200, 404), is color-coded based on the status range.

• Method: The HTTP method (GET, POST, etc.), is displayed in cyan.

• Path: The request path.

• Query Parameters: The query parameters in a key-value format.

• Remote Host: The IP address of the client making the request.

• User Agent: The User-Agent header, which tells you what browser or client made the request.

• Duration: The time it took for the server to process the request.

This gives you a full picture of each request’s lifecycle in a clean and organized format.

4. Color Coding:

The log output uses ANSI escape codes to add colors:

• Green for success (2xx),

• Yellow for redirection (3xx) or unknown status,

• Red for error statuses (4xx, 5xx),

• Cyan for HTTP methods.

These colors help differentiate important information quickly when scanning through logs.

Example Output

When a request comes into the server, the log output would look something like this:

------------------------ Request Details ------------------------
Status: 200
Method: GET
Path: /api/products
Query Params: id=123, sort=asc
Remote Host: 192.168.1.1
User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Duration: 45ms
------------------------------------------------------------------

This format makes it easy to read and troubleshoot requests at a glance, with different colors highlighting important parts of the log.

Benefits of Using Call Logging in Ktor

1. Improved Debugging: Logs provide essential information for troubleshooting failed requests or performance bottlenecks, allowing developers to understand the request flow.

2. Performance Monitoring: Tracking the processing time of each request helps in identifying slow endpoints.

3. Security Insights: Logging details such as the client’s IP and User-Agent can help detect suspicious activity or malformed requests.

4. Customizable Output: As shown in the code, the format can be adjusted to fit your exact logging needs. You can add or remove details based on what’s important for your application.

GitHub Example: https://github.com/piashcse/ktor-E-Commerce
Medium: https://piashcse.medium.com/enhanced-logging-in-ktor-server-with-calllogging-feature



September 28, 2024

RTK Query example code in react-native

September 28, 2024 Posted by Piash , , , No comments

RTK Query is one of the best ways to handle data fetching efficiently, a data-fetching and caching tool that comes with the Redux Toolkit. In this article, we’ll walk through an example of using RTK Query in a React Native application.

We’ll demonstrate how to set up RTK Query to fetch movie data from TheMovieDB API and display it in your React Native app.

Movie with RTK Query

Prerequisites

Before we dive in, ensure you have the following set up:
• A React Native project
• Redux Toolkit installed
• RTK Query integrated into your Redux setup
• TheMovieDB API Key (Sign up at themoviedb.org)

Step 1: Install Dependencies
First, you need to install the required dependencies if you haven’t already:

npm install @reduxjs/toolkit react-redux
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import {Constants} from "../../appconstants/AppConstants.ts";
import {MovieResult} from "../../types/MovieResult.ts";
import {MovieItem} from "../../types/MovieItem.ts";
import {MovieDetail} from "../../types/MovieDetail.ts";
import {CastAndCrew} from "../../types/ArtistAndCrew.ts";
import {ArtistDetail} from "../../types/ArtistDetail.ts";

export const nowPlayingMovieApi = createApi({
reducerPath: 'nowPlayingMovieApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.themoviedb.org/3/' }),
endpoints: (builder) => ({
getNowPlayingMovie: builder.query<MovieItem[], number>({
query: (page) => `movie/now_playing?api_key=${Constants.API_KEY}&language=en-US?page=${page}`,
transformResponse: (response: MovieResult) => response.results
}),
}),
})

export const { useGetNowPlayingMovieQuery } = nowPlayingMovieApi;

export const popularMovieApi = createApi({
reducerPath: 'popularMovieApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.themoviedb.org/3/' }),
endpoints: (builder) => ({
getPopularMovie: builder.query<MovieItem[], number>({
query: (page ) => `movie/popular?api_key=${Constants.API_KEY}&language=en-US?page=${page}`,
transformResponse: (response: MovieResult) => response.results
}),
}),
})


export const {useGetPopularMovieQuery} = popularMovieApi;

export const topRatedMovieApi = createApi({
reducerPath: 'topRatedMovieApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.themoviedb.org/3/' }),
endpoints: (builder) => ({
getTopRatedMovie: builder.query<MovieItem[], number>({
query: (page) => `movie/top_rated?api_key=${Constants.API_KEY}&language=en-US?page=${page}`,
transformResponse: (response: MovieResult) => response.results
}),
}),
})
export const {useGetTopRatedMovieQuery} = topRatedMovieApi;


export const upcomingMovieApi = createApi({
reducerPath: 'upcomingMovieApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.themoviedb.org/3/' }),
endpoints: (builder) => ({
getUpcomingMovie: builder.query<MovieItem[], number>({
query: (page) => `movie/upcoming?api_key=${Constants.API_KEY}&language=en-US?page=${page}`,
transformResponse: (response: MovieResult) => response.results
}),
}),
})

export const {useGetUpcomingMovieQuery} = upcomingMovieApi;

export const movieDetailApi = createApi({
reducerPath: 'movieDetailApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.themoviedb.org/3/' }),
endpoints: (builder) => ({
getMovieDetail: builder.query<MovieDetail, number>({
query: (movieId) => `movie/${movieId}?api_key=${Constants.API_KEY}&language=en-US`,
transformResponse: (response: MovieDetail) => response
}),
}),
})

export const {useGetMovieDetailQuery} = movieDetailApi;


export const similarMovieApi = createApi({
reducerPath: 'similarMovieApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.themoviedb.org/3/' }),
endpoints: (builder) => ({
getSimilarMovie: builder.query<MovieItem[], number>({
query: (movieId) => `movie/${movieId}/recommendations?api_key=${Constants.API_KEY}&language=en-US`,
transformResponse: (response: MovieResult) => response.results
}),
}),
})
export const {useGetSimilarMovieQuery} = similarMovieApi;

export const artistAndCrewApi = createApi({
reducerPath: 'artistAndCrewApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.themoviedb.org/3/' }),
endpoints: (builder) => ({
getArtistAndCrew: builder.query<CastAndCrew, number>({
query: (movieId) => `movie/${movieId}/credits?api_key=${Constants.API_KEY}&language=en-US`,
transformResponse: (response: CastAndCrew) => response
}),
}),
})

export const {useGetArtistAndCrewQuery} = artistAndCrewApi;

export const artistDetailApi = createApi({
reducerPath: 'artistDetailApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.themoviedb.org/3/' }),
endpoints: (builder) => ({
getAristDetail: builder.query<ArtistDetail, number>({
query: (personId) => `person/${personId}?api_key=${Constants.API_KEY}&language=en-US`,
transformResponse: (response: ArtistDetail) => response
}),
}),
})

export const {useGetAristDetailQuery} = artistDetailApi;
import {configureStore} from '@reduxjs/toolkit'
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';
import logger from 'redux-logger';
import {
artistAndCrewApi,
artistDetailApi,
movieDetailApi,
nowPlayingMovieApi,
popularMovieApi,
similarMovieApi,
topRatedMovieApi,
upcomingMovieApi
} from './query/RTKQuery.ts'
import {setupListeners} from "@reduxjs/toolkit/query";

const sagaMiddleware = createSagaMiddleware();
const middleware = [sagaMiddleware];

const configurationAppStore = () => {
const store = configureStore({
reducer: {
[nowPlayingMovieApi.reducerPath]: nowPlayingMovieApi.reducer,
[popularMovieApi.reducerPath]: popularMovieApi.reducer,
[topRatedMovieApi.reducerPath]: topRatedMovieApi.reducer,
[upcomingMovieApi.reducerPath]: upcomingMovieApi.reducer,
[movieDetailApi.reducerPath]: movieDetailApi.reducer,
[similarMovieApi.reducerPath]: similarMovieApi.reducer,
[artistAndCrewApi.reducerPath]: artistAndCrewApi.reducer,
[artistDetailApi.reducerPath]: artistDetailApi.reducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat([...middleware, logger,
nowPlayingMovieApi.middleware,
popularMovieApi.middleware,
topRatedMovieApi.middleware,
upcomingMovieApi.middleware,
movieDetailApi.middleware,
similarMovieApi.middleware,
artistAndCrewApi.middleware,
artistDetailApi.middleware
]),
devTools: process.env.NODE_ENV === 'development'
})
setupListeners(store.dispatch)
sagaMiddleware.run(rootSaga);
return store
}
export default configurationAppStore

Types

import {MovieItem} from "./MovieItem";
import {Dates} from "./Dates";

export interface MovieResult {
dates: Dates
page: number
results: MovieItem[]
total_pages: number
total_results: number
}
export interface MovieItem {
adult: boolean
backdrop_path: string
genre_ids: number[]
id: number
original_language: string
original_title: string
overview: string
popularity: number
poster_path: string
release_date: string
title: string
video: boolean
vote_average: number
vote_count: number
}
export interface Dates {
maximum: string
minimum: string
}

MovieComponent.tsx

import React, {useState} from 'react';
import {FlatList, Image, View, TouchableOpacity, ImageBackground} from "react-native";
import styles from "./MovieListStyle";
import {Constants} from "../../appconstants/AppConstants";
import {MovieItem} from "../../types/MovieItem";

interface MovieItemProps {
movies: Array<MovieItem>;
onPress: (item: MovieItem) => void;
loadMoreData: () => void
}

const MovieComponent = (props: MovieItemProps) => {
const {movies, onPress, loadMoreData} = props;
const [isLoading, setIsLoading] = useState(true)
const movieItem = ({item}: { item: MovieItem }) => {
return (<TouchableOpacity style={styles.movieItemContainer} onPress={() => onPress(item)}>
<ImageBackground
imageStyle={{borderRadius: 18}}
source={isLoading ? require('../../assets/placeholder.jpeg') : {uri: `${Constants.IMAGE_URL}${item.poster_path}`}}
>
<Image
style={styles.imageView}
source={{
uri: `${Constants.IMAGE_URL}${item.poster_path}`,
}}
onLoadEnd={() => {
setIsLoading(false)
}}
/>
</ImageBackground>
</TouchableOpacity>)
};

return (<View style={styles.mainView}>
<FlatList
style={styles.flatListContainer}
data={movies}
renderItem={movieItem}
numColumns={2}
keyExtractor={(item, index) => index.toString()}
onEndReachedThreshold={0.5}
onEndReached={loadMoreData}
/>
</View>);
}

export default MovieComponent

Home.tsx

import React, {useEffect, useState} from 'react';
import Loading from '../../components/loading/Loading';
import MovieComponent from '../../components/movielist/MovieComponent.tsx';
import {View} from 'react-native';
import styles from './HomeStyle'
import {useGetNowPlayingMovieQuery} from "../../redux/query/RTKQuery.ts";
import {useNavigation} from "@react-navigation/native";
import {MovieItem} from "../../types/MovieItem.ts";


const Home = () => {
const navigation = useNavigation();
const [page, setPage] = useState(1);
const [movies, setMovies] = useState<Array<MovieItem>>([]);
const {data, error, isLoading, isFetching} = useGetNowPlayingMovieQuery(page)
useEffect(() => {
if (data && page > 1) {
setMovies((prevMovies) => [...prevMovies, ...data]);
}else {
setMovies(data ?? []);
}
}, [page, data?.length]);

const loadMoreMovies = () => {
if (!isFetching && !isLoading && !error) {
setPage( page + 1);
}
};

if (isLoading) return <Loading/>;

return (<View style={styles.mainView}>
<MovieComponent
movies={movies}
onPress={(item) => navigation.navigate('MovieDetail', {movieId: item.id})}
loadMoreData={loadMoreMovies}/>
</View>);
}
export default Home;

GitHub: https://github.com/piashcse/react-native-movie
Medium: https://piashcse.medium.com/rtk-query-example-code-in-react-native-e9a0bd402d8f